POST /info — spot & margin queries
Read queries for spot markets, leveraged spot margin, and the Earn pool. Same POST /info endpoint and envelope as the base page.
Plain spot token balances are on account_state.
Its spot.balances array carries every token the account holds — USDC and spot
tokens alike — with avg_entry_px per row. There is no separate spot-balance
read.
Spot, spot-margin & Earn query types
Spot pair universe and token registry
Every spot pair and every spot token in the market and token registry.
The spot_meta query type is removed. Query
markets_meta with kind: "spot" instead. A
spot_meta request now returns 400 UNKNOWN_TYPE.
Request
{ "type": "markets_meta", "kind": "spot" }
| Field | Type | Required | Meaning |
|---|---|---|---|
kind | string | yes | "spot" selects the spot section of the registry |
Response
The spot section of the markets_meta response:
{
"data": {
"type": "markets_meta",
"spot": {
"pairs": [
{
"signing_id": 113, "name": "MTF/USDC", "base": 104, "quote": 100,
"sz_decimals": 2, "taker_fee_bps": "3.5", "min_notional": "1",
"active": true, "deployer": "0x17c5…d025", "registered_at": 0,
"mark_px": "0.13787", "mid_px": "0.13787", "day_ntl_vlm": "0",
"prev_day_px": null, "circulating_supply": "10000010"
}
],
"tokens": [
{
"id": 101, "name": "BTC", "sz_decimals": 5, "wei_decimals": 8,
"token_id": "0xab…", "system_address": "0x55…",
"evm_contract": { "address": "0x66…", "variant": 0, "evm_extra_wei_decimals": -3 },
"is_canonical": true, "total_supply": "21000000"
}
]
}
}
}
taker_fee_bps is the OVERRIDE, not the effective fee — and null is not
zero.
A spot pair's deployer may set a taker override. If an override exists it
WINS, for every account, whatever the volume tier says. If none exists, the
volume-tiered fee_schedule applies. That is the
whole resolution rule, and this row is where it is now written down.
Two encodings used to hide it, and both are fixed:
- The value was truncated. The override is stored in deci-bps and rendered by
integer division, so
35deci-bps (3.5 bps) printed as"3". It renders losslessly now —"3.5". - "No override" printed as
"0". Zero reads as "this pair is fee-free" and meant "the schedule applies". It isnullnow.
So null sends you to fee_schedule. A "0", should you see one, is a real
zero-rate override. Measured live before the fix: a pair served "5" while
fee_schedule said "3.5", and nothing on either read explained which one
charged. See
Ids and wire shapes.
A pair row keys on signing_id, and its price fields can be null. A
tradable pair has distinct base / quote (e.g. "MTF/USDC"). A per-token
"self pair" projects the token registry as a pair and has base == quote.
mark_px, mid_px and prev_day_px are sent as null on a pair that has
not traded — the key is present and holds null, it is never omitted. Most of
the registry reads null here at any moment, so treat null as the normal
answer, not as an error.
| Field | Type | Meaning |
|---|---|---|
pairs[*].signing_id | uint32 | The number you put in the EIP-712 market field when you sign a spot order for this pair. The field was named id; that name is gone. Every read keys the pair by name — see below |
pairs[*].name | string | Pair name (e.g. "MTF/USDC") |
pairs[*].base / quote | uint32 | Base / quote asset id (equal for self-pairs) |
pairs[*].sz_decimals | uint8 | Size precision of the pair's base leg |
pairs[*].taker_fee_bps | bps string | null | The pair's deployer taker override, decimal bps. null means there is no override and the volume-tiered fee_schedule applies — see the resolution rule below |
pairs[*].min_notional | Decimal string | Min notional (whole USDC); "0" if unset |
pairs[*].active | bool | Whether the pair is active for trading |
pairs[*].deployer | hex address | The account that registered the pair |
pairs[*].registered_at | uint64 | Block height at which the pair was registered. 0 on a genesis pair |
pairs[*].mark_px | Decimal string | null | Last-trade price (whole USDC); null before the first trade |
pairs[*].mid_px | Decimal string | null | Book mid, falls back to mark_px; null when neither exists |
pairs[*].day_ntl_vlm | Decimal string | 24h notional volume |
pairs[*].prev_day_px | Decimal string | null | Price ~24h ago; null if unknown. It reads null on a traded pair too, until 24 hours of history exist |
pairs[*].circulating_supply | Decimal string | Base token committed supply (whole units) |
tokens[*].id | uint32 | Spot token asset id |
tokens[*].name | string | Token name (e.g. "USDC", "MTF") |
tokens[*].sz_decimals | uint8 | Display / size precision |
tokens[*].wei_decimals | uint8 | Native (ERC-20-style) token decimals |
tokens[*].token_id | hex string (32 bytes) | Canonical token id, 0x-hex |
tokens[*].system_address | hex address | Core-side anchor address |
tokens[*].evm_contract | object | null | EVM binding; null when the token binds nothing |
tokens[*].evm_contract.address | hex address | The ERC-20 the asset is bound to. Permanent once written — see Rules |
tokens[*].evm_contract.variant | uint8 | How the token is bound: 0 a deployed contract, 1 first-storage-slot, 2 custom-storage-slot. It does not change whether the asset can cross |
tokens[*].evm_contract.evm_extra_wei_decimals | int8 | The deployer's declared decimal offset. It has no effect on a credit — see Rules |
tokens[*].is_canonical | bool | Canonical (genesis / governance-listed) token |
tokens[*].total_supply | Decimal string | Committed token issuance (whole units); "0" when none |
Rules
tokensandpairsare sorted by key (asset id / pair id).evm_contractreports the BINDING, never a declaration.register_tokenaccepts anevm_contractfield from the caller and stores it unvalidated, but no transfer path reads it. The address served here comes from the EVM binding registry — the same source the Core-to-EVM transfer checks — so this read never offers a contract the chain would refuse. A token whose deployer declared a contract that was never bound reportsnull. See which assets can cross.evm_extra_wei_decimalsis the deployer's declared value. It has no effect on a credit — a credit lands in the token'swei_decimals, the sibling field.- A binding is first-write-wins and PERMANENT. ⚠️ Corrected — an earlier
version of this page said the address rotates. It does not. The binding vote
refuses an asset that already has a binding, and refuses a contract already
bound to another asset. Nothing removes a binding. So an address you read here
stays valid for that asset. Still read it rather than hard-coding it: a token
can gain its FIRST binding at any time, and
nulltoday is notnulltomorrow. Key your own records ontokens[*].id. - The credit scale is the token's
wei_decimals, not the contract's. A binding does not carry a decimals value of its own, and it cannot change the one the token was registered with. Readtokens[*].wei_decimalsto size a transfer.
Every spot-margin position for an account
Every spot-margin position held by one account.
Cross-collateralized. Read surface for leveraged spot margin; the position's margin is held against your one unified USDC account, not a per-pair bucket. See the concept page for the model.
Request
{ "type": "spot_margin_state", "user": "0x<addr>" }
| Field | Type | Required | Meaning |
|---|---|---|---|
user | hex address | yes | Account to read |
Response
{
"data": {
"type": "spot_margin_state",
"user": "0x<addr>",
"accounts": [
{
"pair": "MTF/USDC",
"collateral": "0",
"borrowed": "20",
"borrow_index_snapshot": "1",
"base_held": "9.99",
"current_debt": "22",
"params": { "init_bps": "2000", "maint_bps": "1000" }
}
]
}
}
| Field | Type | Meaning |
|---|---|---|
accounts[*].pair | string | Spot pair symbol (e.g. "MTF/USDC"), not a numeric id |
accounts[*].collateral | Decimal string | Vestigial. Spot margin is now cross-collateralized against your unified USDC account, so there is no per-pair collateral bucket. Reads "0"; kept only for wire-shape compatibility |
accounts[*].borrowed | Decimal string | Outstanding loan principal (at the snapshot index) |
accounts[*].borrow_index_snapshot | Decimal string | Pool borrow index captured at open (debt-accrual basis) |
accounts[*].base_held | Decimal string | Segregated base bought on leverage (not in spendable balances) |
accounts[*].current_debt | Decimal string | Debt accrued to now: borrowed × (pool_index / snapshot) |
accounts[*].params | object | null | Per-pair { init_bps, maint_bps }; null = margin not enabled / uncalibrated for the pair |
Rules
- Positions are listed in pair-id order.
- An account with no positions returns an empty
accountsarray.
Earn lending pools and account stake
Every Earn lending pool, plus one account's stake when user is supplied.
Live on testnet. Read surface for the Earn lending pools. The pool list is empty until the first deposit creates one.
Request
{ "type": "earn_state", "user": "0x<addr>" }
| Field | Type | Required | Meaning |
|---|---|---|---|
user | hex address | no | Include this account's stake in the response |
Response
{
"data": {
"type": "earn_state",
"pools": [
{
"name": "USDC",
"signing_id": 100,
"total_supplied": "1000",
"total_borrowed": "20",
"idle": "980",
"shares_total": "1000",
"share_value": "1",
"borrow_index": "1",
"reserve_factor_bps": "1000",
"borrow_rate_bps_annual": 0,
"reserve_accrued": "0",
"user_shares": "100",
"user_value": "100"
}
]
}
}
| Field | Type | Meaning |
|---|---|---|
pools[*].name | string | Pool token name (e.g. "USDC") |
pools[*].signing_id | uint32 | Lendable asset id — the pool key, and the number you sign an Earn action with |
pools[*].total_supplied | Decimal string | Pool NAV — supplied principal plus folded-in repaid interest |
pools[*].total_borrowed | Decimal string | Quote currently lent to spot-margin borrowers |
pools[*].idle | Decimal string | total_supplied − total_borrowed — the instantly-withdrawable bound |
pools[*].shares_total | Decimal string | Total shares outstanding |
pools[*].share_value | Decimal string | total_supplied / shares_total (0 when no shares) |
pools[*].borrow_index | Decimal string | Cumulative borrow index (debt-accrual basis) |
pools[*].reserve_factor_bps | bps string | Protocol cut of borrow interest, whole bps. It is a string, not a number |
pools[*].borrow_rate_bps_annual | bps string | Annualised borrow rate, whole bps. It is a string, not a number. "0" on every live pool today — see Rules |
pools[*].reserve_accrued | Decimal string | Protocol reserve accumulated from interest |
pools[*].user_shares | Decimal string | Only with user — shares the account holds in the pool |
pools[*].user_value | Decimal string | Only with user — user_shares × share_value |
user_shares / user_value are ABSENT without user, not zero. A request
that omits user drops both keys from every pool row. A request that sends
user carries both on every pool, and they read "0" for a pool the account
has not supplied. Absent means "not asked"; "0" means "asked, and the stake is
nothing". Test for key presence before you read a stake.
Rules
- Pools are listed in asset-id order.
- Omitting
userdrops theuser_shares/user_valuefields. poolsis empty until the first deposit creates a pool. An empty array is the honest answer for a chain with no Earn supply, not a failure.- A
borrow_rate_bps_annualof0means the pool pays nothing, andshare_valuewill not move. A pool auto-creates at rate0, and the per-block accrual stamps the time without steppingborrow_index. Do not compute an APY from a risingshare_valuethat is not rising. A governance vote sets a non-zero rate — see Earn.
Borrow interest an account owes
Every open borrow one account carries, split into principal and interest.
This read is keyed on the ACCOUNT, not on the lane that charged the
interest. Spot margin is one lane that can create a debt and an Earn pool is
one place that funds it, but neither is the debt. A second lane that charges
interest later joins the borrows array — it does not get a second query type.
So build an "Interest" view on this read, not on
spot_margin_state.
Request
{ "type": "user_interest", "user": "0x<addr>" }
| Field | Type | Required | Meaning |
|---|---|---|---|
user | hex address | yes | Account to read |
Response
{
"data": {
"type": "user_interest",
"user": "0x<addr>",
"owed": "2",
"borrows": [
{
"lane": "spot_margin",
"pair": "MTF/USDC",
"principal": "20",
"accrued": "22",
"interest": "2",
"index_snapshot": "1",
"pool_index": "1.1"
}
],
"earned": null
}
}
| Field | Type | Meaning |
|---|---|---|
owed | Decimal string | Sum of borrows[*].interest, whole-USDC |
borrows[*].lane | string | What created the debt. "spot_margin" is the only value today |
borrows[*].pair | string | Spot pair symbol (e.g. "MTF/USDC"), not a numeric id |
borrows[*].principal | Decimal string | Loan principal, before interest |
borrows[*].accrued | Decimal string | What the debt is worth now — principal plus interest |
borrows[*].interest | Decimal string | accrued − principal |
borrows[*].index_snapshot | Decimal string | Pool borrow index captured at open — the basis interest is measured FROM |
borrows[*].pool_index | Decimal string | null | The pool's borrow index now. null when the pair has no committed pool |
earned | null | Always null — see Rules |
Rules
- Both indices are in the answer so you can check the arithmetic.
accrued = principal × pool_index / index_snapshot. Divide before you multiply: the chain's settlement path does, and on a non-terminating ratio the other order lands on a different last digit. - An
index_snapshotof0reads as1. It is the pre-accrual basis a position gets before the pool has ever accrued, not a division by zero. - A
pool_indexofnullmeans nothing can accrue. The pair has no committed pool, soaccruedequalsprincipalandinterestis"0". - Borrows are listed in pair-id order. A borrow whose principal is zero is not listed — this read answers what you owe.
- An account with no borrows returns an empty
borrowsarray andowed"0". earnedis alwaysnull, and that is not a placeholder. The chain stores a supplier's shares but no cost basis, so lending profit is not derivable from committed state and this read will not invent it. For the supplier side readearn_statewithuser:user_sharesanduser_valuegive the stake and what it is worth now. Track your own deposits if you need a profit figure.
Spot-pair-deploy gas-auction state
MIP-1 spot-pair-deploy gas-auction state.
spot_deploy_auction is the live name. spot_deploy_state is removed. The
old name answers 400 with error.code UNKNOWN_TYPE.
Request
{ "type": "spot_deploy_auction" }
No parameters.
Response
{
"data": {
"type": "spot_deploy_auction",
"current_ask": "100",
"floor": "100",
"start": "100",
"start_multiplier": 2,
"duration_blocks": 100,
"opened_at_block": 0,
"now_block": 26435840,
"last_clearing": "0",
"sealed_round": {
"auction_round": 0,
"current_bid": "0",
"current_winner": null,
"auction_end": 0,
"started_at": 0,
"total_burned": "0",
"deposit": "0"
}
}
}
This is a descending-clock (Dutch) auction, and the top level is the clock.
The ask starts at start and decays toward floor over duration_blocks,
measured from opened_at_block. An accept clears immediately at the clock
ask, so a bidder submits at or above current_ask.
Read current_ask fresh on every accept; never cache it. It falls every
block, and the chain clears against its own clock, not against the value you
read a minute ago.
| Field | Type | Meaning |
|---|---|---|
current_ask | Decimal string | The price to deploy a pair right now, whole USDC. It falls every block |
floor | Decimal string | The lowest price the decay reaches. current_ask never goes below it |
start | Decimal string | The price the current decay opened at |
start_multiplier | uint | Demand-adaptive multiplier that sets the next round's start price |
duration_blocks | uint64 | Length of one decay, in blocks |
opened_at_block | uint64 | Block height at which the current decay opened. 0 = no round has opened yet |
now_block | uint64 | The chain height this answer was read at — the clock current_ask is priced against |
last_clearing | Decimal string | Price the previous deploy cleared at. "0" = nothing has cleared yet |
sealed_round | object | The sealed-bid round, described below |
sealed_round is a SECOND auction, not a view of the clock. It is the
sealed-bid round that runs beside the descending clock. Its current_bid and
current_winner come from submitted bids; its total_burned and deposit come
from settled rounds. None of them price the clock. A caller that reads
sealed_round.current_bid as the deploy price pays the wrong number — read
current_ask for that.
The sealed-bid fields moved here, and lost the _ms suffixes. They were
once at the top level as auction_end_ms and started_at_ms. Both names are
gone. Read them inside sealed_round as auction_end and started_at.
| Field | Type | Meaning |
|---|---|---|
sealed_round.auction_round | uint64 | Current sealed-bid round |
sealed_round.current_bid | Decimal string | Leading bid |
sealed_round.current_winner | hex address | null | Current high bidder. null when nobody has bid |
sealed_round.auction_end | uint64 | Round close (consensus ms). 0 = no round is open |
sealed_round.started_at | uint64 | Round start (consensus ms). 0 = no round is open |
sealed_round.total_burned | Decimal string | Cumulative burned winning-bid notional |
sealed_round.deposit | Decimal string | Total escrowed deposit (base units) |
See also
POST /info— the base read endpoint (envelope, conventions, account & infra queries)- Perpetual queries — perp-market reads
- Spot / Spot margin — the products