Domain
| Field | Value |
|---|---|
name | PropAMMExecutor |
version | 2 |
chainId | the chain the board lives on |
verifyingContract | 0x000000fFA5f8Ae192Ab65204f9B7E062CbF4e05D |
| Item | Value |
|---|---|
| Digest | keccak256(0x1901 || domainSeparator || structHash) |
| EOA signer | 65-byte ECDSA signature |
| Contract signer | EIP-1271 isValidSignature |
| Onchain helpers | DOMAIN_SEPARATOR(), hashLadder, hashOffsetLadder, hashAnchor, hashControls, ladderDigest, offsetLadderDigest, anchorDigest, 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 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 Anchor {
address mm;
address tokenIn;
address tokenOut;
uint256 price; // 1e18-scaled tokenOut per tokenIn
uint256 nonce; // anchor nonce, independent of the depth nonce
uint256 timestamp; // unix seconds the maker priced this anchor; the drift origin
uint256 expiresAt; // unix seconds; the board is dark past this
}
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 commit; 0 for none
}
PriceLadder(address mm,address provider,address tokenIn,address tokenOut,Level[] levels,uint256 nonce,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)
Anchor(address mm,address tokenIn,address tokenOut,uint256 price,uint256 nonce,uint256 timestamp,uint256 expiresAt)
BoardControls(address mm,address tokenIn,address tokenOut,uint256 nonce,uint256 blockCap,uint256 widenPpmPerSqrtSecond,uint256 premiumPpm,uint256 premiumBlocks)
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: "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" },
],
Anchor: [
{ name: "mm", type: "address" }, { name: "tokenIn", type: "address" },
{ name: "tokenOut", type: "address" }, { name: "price", type: "uint256" },
{ name: "nonce", type: "uint256" }, { name: "timestamp", type: "uint256" },
{ name: "expiresAt", type: "uint256" },
],
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" },
],
} as const;
const domain = { name: "PropAMMExecutor", version: "2", chainId, verifyingContract: executor };
| Message | provider | expiresAt |
|---|---|---|
PriceLadder, OffsetLadder | yes, binds the provider for the board | yes |
Anchor | no, uses the board’s provider | yes |
BoardControls | no, uses the board’s provider | no, 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 |
price = anchor.price * (1e6 - offsetPpm - driftPpmPerSecond * (block.timestamp - anchor.timestamp)) / 1e6, floored
1e6 drops off with every deeper rung. Without its first rung the board stops quoting.
| Anchor age | Discount (offset 500 ppm, drift 2 ppm/s) | 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 and the executor both reject a message that fails any of these.| Rule | Applies to |
|---|---|
tokenIn != 0, tokenOut != 0, tokenIn != tokenOut, provider != 0 | ladders |
nonce fits uint128, expiresAt fits uint40 | ladders |
expiresAt in the future at commit time, at most 1 hour ahead | ladders and anchors |
| 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 |
nonce fits uint48 | anchors and controls |
price above zero and fits uint128 | anchors |
timestamp <= expiresAt | anchors |
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
| Nonce | Scope | Rule |
|---|---|---|
depth (PriceLadder.nonce, OffsetLadder.nonce) | one per board, shared by both ladder forms | strictly greater than the stored depth nonce; fits uint128 |
anchor (Anchor.nonce) | one per board, independent | strictly greater than the stored anchor nonce; 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. Unix milliseconds fit every limit. 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.
Wire format
| Item | Format |
|---|---|
| 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",
"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",
"tokenIn": "0x8b414aD7005EeFd315aF2A16538885Eae229bab7",
"tokenOut": "0xAbbdbbbd6d56593A9c5656c06cB30D61E4a544Df",
"price": "2000000000000000000000",
"nonce": "88213",
"timestamp": "1753290000",
"expiresAt": "1753290030",
"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",
"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 |
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 (uint128) for ladders and offsets, uint48 for anchors and controls |
timestamp | unix seconds as string | anchors only; when the maker priced it; not after expiresAt |
expiresAt | unix seconds as string | uint40; at most 1 hour ahead |
price | uint128 as string | anchors only; positive |
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 its expiresAt |
| 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 already in the past | check clock skew; expiresAt is in seconds |
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 | strictly increasing nonces |
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 |
expiresAt | in the future, at most 1 hour ahead |