Domain
| Field | Value |
|---|---|
name | PropAMMExecutor |
version | 2 |
chainId | the chain the board lives on |
verifyingContract | 0x0000006f9bBA066e4efda2e0CAbE4FF8cf9030aC |
| Item | Value |
|---|---|
| Digest | keccak256(0x1901 || domainSeparator || structHash) |
| EOA signer | 65-byte ECDSA signature |
| Contract signer | EIP-1271 isValidSignature, checked after ECDSA with a 150,000 gas budget |
| Onchain helpers | DOMAIN_SEPARATOR(), hashLadder, hashOffsetLadder, hashAnchorEntry, hashAnchorBatch, hashControls, ladderDigest, offsetLadderDigest, anchorBatchDigest, controlsDigest |
Structs
Field order is encoding order.struct Level {
uint256 size; // cumulative tokenIn volume available up to this level
uint256 price; // 1e18-scaled tokenOut per tokenIn for volume landing in this level
}
struct PriceLadder {
address mm; // signing address; the board key
address provider; // the IMMProvider contract that fills it; signed, so callers cannot swap it
address tokenIn;
address tokenOut;
Level[] levels; // ascending cumulative sizes, prices non-improving with depth
uint256 nonce; // depth nonce, strictly increasing per (mm, tokenIn, tokenOut)
uint256 timestampMs; // unix milliseconds the maker priced it; the board ages from here
uint256 expiresAt; // unix seconds
}
struct Rung {
uint256 size; // cumulative tokenIn volume available up to this rung
uint256 offsetPpm; // discount below the anchor, parts per million
}
struct OffsetLadder {
address mm;
address provider;
address tokenIn;
address tokenOut;
Rung[] rungs; // ascending cumulative sizes, offsets non-decreasing with depth
uint256 nonce; // depth nonce, shared with price ladders for the same board
uint256 expiresAt; // unix seconds
uint256 driftPpmPerSecond; // offset widening per second of anchor age; 0 for none
}
struct AnchorEntry {
address tokenIn; // the direction `price` is quoted for
address tokenOut;
uint256 price; // 1e18-scaled tokenOut per tokenIn
uint256 timestampMs; // unix milliseconds the maker priced this anchor; strictly increasing per pair
uint256 ttl; // seconds the anchor lives after timestampMs
uint256 skewPpm; // extra discount on the tokenIn -> tokenOut side, ppm
uint256 reverseSkewPpm; // extra discount on the tokenOut -> tokenIn side, ppm
}
struct AnchorBatch {
address mm; // signing address; the board key for every entry
AnchorEntry[] entries; // one per pair
}
struct BoardControls {
address mm;
address tokenIn;
address tokenOut;
uint256 nonce; // controls nonce, its own sequence
uint256 blockCap; // most tokenIn one block may fill from this board; 0 for no cap
uint256 widenPpmPerSqrtSecond; // extra discount per square root of a second of quote age; 0 for none
uint256 premiumPpm; // extra discount inside the premium window; 0 for none
uint256 premiumBlocks; // blocks the premium covers after a depth, anchor or controls commit; 0 for none
uint256 expiresAt; // unix seconds; the last second this message can be committed
}
PriceLadder(address mm,address provider,address tokenIn,address tokenOut,Level[] levels,uint256 nonce,uint256 timestampMs,uint256 expiresAt)Level(uint256 size,uint256 price)
OffsetLadder(address mm,address provider,address tokenIn,address tokenOut,Rung[] rungs,uint256 nonce,uint256 expiresAt,uint256 driftPpmPerSecond)Rung(uint256 size,uint256 offsetPpm)
AnchorBatch(address mm,AnchorEntry[] entries)AnchorEntry(address tokenIn,address tokenOut,uint256 price,uint256 timestampMs,uint256 ttl,uint256 skewPpm,uint256 reverseSkewPpm)
BoardControls(address mm,address tokenIn,address tokenOut,uint256 nonce,uint256 blockCap,uint256 widenPpmPerSqrtSecond,uint256 premiumPpm,uint256 premiumBlocks,uint256 expiresAt)
types object for viem or ethers signTypedData:
const types = {
Level: [{ name: "size", type: "uint256" }, { name: "price", type: "uint256" }],
PriceLadder: [
{ name: "mm", type: "address" }, { name: "provider", type: "address" },
{ name: "tokenIn", type: "address" }, { name: "tokenOut", type: "address" },
{ name: "levels", type: "Level[]" }, { name: "nonce", type: "uint256" },
{ name: "timestampMs", type: "uint256" }, { name: "expiresAt", type: "uint256" },
],
Rung: [{ name: "size", type: "uint256" }, { name: "offsetPpm", type: "uint256" }],
OffsetLadder: [
{ name: "mm", type: "address" }, { name: "provider", type: "address" },
{ name: "tokenIn", type: "address" }, { name: "tokenOut", type: "address" },
{ name: "rungs", type: "Rung[]" }, { name: "nonce", type: "uint256" },
{ name: "expiresAt", type: "uint256" }, { name: "driftPpmPerSecond", type: "uint256" },
],
AnchorEntry: [
{ name: "tokenIn", type: "address" }, { name: "tokenOut", type: "address" },
{ name: "price", type: "uint256" }, { name: "timestampMs", type: "uint256" },
{ name: "ttl", type: "uint256" }, { name: "skewPpm", type: "uint256" },
{ name: "reverseSkewPpm", type: "uint256" },
],
AnchorBatch: [{ name: "mm", type: "address" }, { name: "entries", type: "AnchorEntry[]" }],
BoardControls: [
{ name: "mm", type: "address" }, { name: "tokenIn", type: "address" },
{ name: "tokenOut", type: "address" }, { name: "nonce", type: "uint256" },
{ name: "blockCap", type: "uint256" }, { name: "widenPpmPerSqrtSecond", type: "uint256" },
{ name: "premiumPpm", type: "uint256" }, { name: "premiumBlocks", type: "uint256" },
{ name: "expiresAt", type: "uint256" },
],
} as const;
const domain = { name: "PropAMMExecutor", version: "2", chainId, verifyingContract: executor };
primaryType: "AnchorBatch": one signature covers every entry.
| Message | provider | Expiry |
|---|---|---|
PriceLadder, OffsetLadder | yes, binds the provider for the board | expiresAt |
AnchorBatch | no, uses each board’s provider | per entry, timestampMs / 1000 + ttl |
BoardControls | no, uses the board’s provider | expiresAt bounds the commit only; once committed, holds until a higher controls nonce |
Prices and sizes
| Item | Rule |
|---|---|
| Output | amountOut = amountIn * price / 1e18, smallest units |
| Price | humanPrice * 1e18 * 10^(decimalsOut - decimalsIn) |
| Size | cumulative tokenIn, smallest units |
| Example | Price |
|---|---|
| WETH (18) to USDC (6) at 2,000 | 2000e6 |
| USDC (6) to WETH (18) at 2,000 | 5e26 |
| same decimals | humanPrice * 1e18 |
2000e6 prices USDC to WETH at 1e36 / 2000e6 = 5e26. You do not sign the reverse direction separately.
Offset rung price, evaluated onchain at fill time:
anchor = entry.price on the signed direction, 1e36 / entry.price (rounded down) on the reverse
skew = skewPpm on the signed direction, reverseSkewPpm on the reverse
age = block.timestamp - timestampMs / 1000, in whole seconds
price = anchor * (1e6 - offsetPpm - driftPpmPerSecond * age - skew) / 1e6, floored
ceil(sqrt(widenPpmPerSqrtSecond^2 * age)) ppm, with age counted from the anchor’s timestampMs on an offset ladder and from the ladder’s own timestampMs on a price ladder, or from the commit when timestampMs is ahead of it. A controls commit does not restart the age. A rung whose discount reaches 1e6 drops off with every deeper rung. Without its first rung the board stops quoting. Skew applies only to offset ladders; a price ladder ignores anchors.
| Anchor age | Discount (offset 500 ppm, drift 2 ppm/s, skew 0) | First rung, anchor 2000e18 |
|---|---|---|
| 0 s | 500 ppm | 1999e18 |
| 30 s | 560 ppm | 1998.88e18 |
| 300 s | 1100 ppm | 1997.8e18 |
Validation rules
The stream rejects a message that fails any of these. The executor skips it and emitsCommitRejected.
| Rule | Applies to |
|---|---|
tokenIn != 0, tokenOut != 0, tokenIn != tokenOut, provider != 0 | ladders |
nonce fits uint48, expiresAt fits uint40 | ladders |
expiresAt in the future at commit time, at most 1 hour ahead | ladders |
timestampMs above zero, at most 30 s ahead, timestampMs / 1000 not after expiresAt | price ladders |
| 1 to 20 levels or rungs | ladders |
sizes strictly ascending, first size above zero, every size fits uint128 | ladders |
prices above zero, each at most the previous price, every price fits uint128 | price ladders |
offsets at least the previous offset and below 1_000_000 | offset ladders |
driftPpmPerSecond fits uint32 | offset ladders |
tokenIn != 0, tokenOut != 0, tokenIn != tokenOut | anchor entries |
price above zero and fits uint120 | anchor entries |
timestampMs fits uint48, at most 2 s ahead | anchor entries |
ttl above zero, at most 3600 | anchor entries |
skewPpm, reverseSkewPpm below 1_000_000 | anchor entries |
timestampMs / 1000 + ttl in the future at commit time | anchor entries |
| 1 to 32 entries, at most one per pair | anchor messages, stream only |
| every entry names a pair registered for you, in either direction | anchor messages, stream only |
nonce fits uint48, expiresAt fits uint40 | controls |
expiresAt not in the past at commit time, at most 1 hour ahead; otherwise ControlsExpired() (CommitRejected kind 4) and the nonce is not consumed | controls |
blockCap fits uint128, widenPpmPerSqrtSecond fits uint32, premiumPpm below 1_000_000, premiumBlocks fits uint16 | controls |
signature verifies for mm under the executor’s domain for chainId | all |
provider equals the provider registered for you on that pair | ladders, stream only |
Nonce rules
| Ordering key | Scope | Rule |
|---|---|---|
depth (PriceLadder.nonce, OffsetLadder.nonce) | one per board, shared by both ladder forms | strictly greater than the stored depth nonce; fits uint48 |
anchor (AnchorEntry.timestampMs) | one per pair, whichever direction is signed | strictly greater than the stored timestampMs for the pair; fits uint48 |
controls (BoardControls.nonce) | one per board, independent | strictly greater than the stored controls nonce; fits uint48 |
STALE_NONCE on the stream, including after expiry. An anchor message returns STALE_NONCE only when every entry in it is stale. Unix milliseconds fit every limit; microseconds and nanoseconds do not fit the depth nonce. Nonces are per board, so one value can be reused across pairs and directions. A depth commit resets the fill count; anchors and controls leave it unchanged. board() reports the pair’s latest timestampMs as anchorNonce.
Wire format
| Item | Format |
|---|---|
| Connection | API key in the x-api-key header; HTTP 401 without a valid key |
| Frames | JSON text |
| Numbers | decimal strings (JSON integers accepted) |
| Addresses | hex |
chainId | number |
{ "type": "subscribe", "data": { "type": "price-ledger", "mm": "0x1111111111111111111111111111111111111111" } }
- price-ladder
- offsets
- anchor
- board-controls
{
"type": "price-ladder",
"payload": {
"mm": "0x1111111111111111111111111111111111111111",
"provider": "0x2222222222222222222222222222222222222222",
"tokenIn": "0x8b414aD7005EeFd315aF2A16538885Eae229bab7",
"tokenOut": "0xAbbdbbbd6d56593A9c5656c06cB30D61E4a544Df",
"levels": [
{ "size": "1000000000000000000", "price": "1998000000000000000000" },
{ "size": "3000000000000000000", "price": "1995000000000000000000" }
],
"nonce": "1753290000123",
"timestampMs": "1753290000123",
"expiresAt": "1753290030",
"signature": "0x…65 bytes…",
"chainId": 84532
}
}
{
"type": "offsets",
"payload": {
"mm": "0x1111111111111111111111111111111111111111",
"provider": "0x2222222222222222222222222222222222222222",
"tokenIn": "0x8b414aD7005EeFd315aF2A16538885Eae229bab7",
"tokenOut": "0xAbbdbbbd6d56593A9c5656c06cB30D61E4a544Df",
"rungs": [
{ "size": "1000000000000000000", "offsetPpm": "1000" },
{ "size": "3000000000000000000", "offsetPpm": "2500" }
],
"nonce": "1753290000124",
"expiresAt": "1753293600",
"driftPpmPerSecond": "10",
"signature": "0x…65 bytes…",
"chainId": 84532
}
}
{
"type": "anchor",
"payload": {
"mm": "0x1111111111111111111111111111111111111111",
"entries": [
{
"tokenIn": "0x8b414aD7005EeFd315aF2A16538885Eae229bab7",
"tokenOut": "0xAbbdbbbd6d56593A9c5656c06cB30D61E4a544Df",
"price": "2000000000000000000000",
"timestampMs": "1753290000000",
"ttl": "30",
"skewPpm": "0",
"reverseSkewPpm": "0"
}
],
"signature": "0x…65 bytes…",
"chainId": 84532
}
}
{
"type": "board-controls",
"payload": {
"mm": "0x1111111111111111111111111111111111111111",
"tokenIn": "0x8b414aD7005EeFd315aF2A16538885Eae229bab7",
"tokenOut": "0xAbbdbbbd6d56593A9c5656c06cB30D61E4a544Df",
"nonce": "88214",
"blockCap": "5000000000000000000",
"widenPpmPerSqrtSecond": "100",
"premiumPpm": "500",
"premiumBlocks": "1",
"expiresAt": "1753290300",
"signature": "0x…65 bytes…",
"chainId": 84532
}
}
| Field | Type | Notes |
|---|---|---|
mm | address | signing key; the board key. Must equal the subscribed maker and the recovered signer |
provider | address | ladders and offsets only; must equal the provider registered for you on the pair |
tokenIn, tokenOut | address | real ERC20 addresses, distinct; in an anchor entry, the direction price is quoted for |
levels[].size, rungs[].size | uint128 as string | cumulative tokenIn depth, strictly ascending |
levels[].price | uint128 as string | 1e18-scaled tokenOut per tokenIn, non-increasing with depth |
rungs[].offsetPpm | string | discount below the anchor in ppm, non-decreasing with depth, below 1e6 |
driftPpmPerSecond | uint32 as string | offset widening per second of anchor age; "0" disables |
nonce | string | depth nonce (uint48) for ladders and offsets, uint48 for controls |
timestampMs | unix milliseconds as string | price ladders only; when the maker priced it, the origin of age fade; above zero, at most 30 s ahead, not after expiresAt |
expiresAt | unix seconds as string | ladders, offsets and controls; uint40; at most 1 hour ahead. For controls, the last second they can be committed |
entries | array | anchors only; 1 to 32 entries, at most one per pair |
entries[].price | uint120 as string | 1e18-scaled tokenOut per tokenIn for the signed direction; positive |
entries[].timestampMs | unix milliseconds as string | when the maker priced it; the drift origin and the ordering key; uint48 |
entries[].ttl | seconds as string | the anchor lives ttl seconds after timestampMs; 1 to 3600 |
entries[].skewPpm, entries[].reverseSkewPpm | string | extra discount on the signed side and on the reverse side, ppm, below 1e6; "0" disables |
blockCap | uint128 as string | controls only; "0" for no cap |
widenPpmPerSqrtSecond, premiumPpm | uint32 as string | controls only; ppm, below 1e6; "0" disables |
premiumBlocks | uint16 as string | controls only; "0" disables |
signature | hex | 65-byte EIP-712 signature by mm |
chainId | number | the chain the board lives on |
Responses and error codes
| Response | Meaning |
|---|---|
{ "type": "ack" } | validated and stored; Biconomy commits it onchain |
{ "type": "error", "code", "message" } | rejected; the last accepted message for that board stays in force until it expires |
| Code | Meaning | Fix |
|---|---|---|
INVALID_JSON | frame is not valid JSON | check encoding |
INVALID_MESSAGE | schema mismatch, or a validation rule above failed | see the rules |
NOT_SUBSCRIBED | a board message before the subscribe ack | subscribe first |
MARKET_MAKER_MISMATCH | payload mm differs from the subscribed mm | one signer per connection |
RATE_LIMITED | over 300 board messages per second on the connection | back off |
UNSUPPORTED_CHAIN | chainId not served by this environment | check the endpoint |
UPDATE_EXPIRED | expiresAt, or an anchor entry’s timestampMs / 1000 + ttl, already in the past | check clock skew and units |
INVALID_TOKEN_PAIR | tokenIn equals tokenOut | fix the pair |
UNREGISTERED_MARKET_MAKER | signer not registered | complete registration |
INVALID_SIGNATURE | recovered signer is not mm | check domain version 2, executor address, chainId, types |
UNSUPPORTED_PAIR | pair or direction not registered for you on that chain | register it |
PROVIDER_MISMATCH | signed provider differs from the registered one | sign the registered provider |
STALE_NONCE | nonce at or below the last accepted one for this board; for anchors, every entry at or below the pair’s last timestampMs | strictly increasing nonces and timestamps |
STORE_FAILED | transient server-side failure | safe to continue |
Limits
| Limit | Value |
|---|---|
| Rate limit | 300 board messages per second per connection (token bucket, 300 burst) |
| Frame size | 64 KB |
| Idle timeout | 60 s without inbound traffic; server pings every 10 s; { "type": "ping" } accepted |
| Levels or rungs per message | 20 |
| Entries per anchor message | 32 |
expiresAt | in the future, at most 1 hour ahead |
Anchor ttl | at most 3600 s |