Skip to main content

POST /info

The info endpoint returns exchange, market and account data. This page covers its envelope, its conventions and the list of query types.

info

Status. stable shape. New query types are added over time. The envelope is committed.

info

Every account-scoped query takes the account as address. Four queries first shipped with user: broker_state, referral_state, spot_margin_state and user_interest. These four still accept user, and their replies carry the account under both names. Send address in new code.

Overview​

One endpoint serves every read. It dispatches on the type field of the request body. It is read-only. It never changes state and never requires a signature.

tip

Each product has its own page of queries:

This page covers the envelope, the conventions, and the account, vault and validator reads.

URL​

POST https://api.<net>.mtf.exchange/info
PathWire shape
POST /info (gateway)MTF-native (this document)

The gateway serves the MTF-native /info. A node that you run yourself serves the same native /info at http://localhost:8080.

Envelope​

Every /info response is one envelope. A success carries data. A failure carries error. The two keys never appear together.

Request

{ "type": "<query_type>", /* type-specific args */ }

Success. Status 200 OK. The type discriminator is echoed inside data:

{
"data": {
"type": "<query_type>",
/* type-specific payload */
}
}

The payload fields keep their old path. A field that you read at body.data.fills before is still at body.data.fills. Only type moved. It was a sibling of data, and it is now the first key of data.

Every read carries type inside data, and this includes the history-archive reads. The archive lane differs from this envelope in one place only: the shape of a rejection. See the archive lane.

A success has no error key. Test whether the key is present. Do not test error === null.

A data of null is a success. A read can succeed with no content. It answers {"data": null} with status 200. Treat it as an empty result. It is not a failure.

Failure. The body has no data key. The HTTP status is the one that the code maps to:

{
"error": {
"code": "UNKNOWN_TYPE",
"message": "unknown info type: markest"
}
}
FieldPresenceMeaning
codealwaysThe stable contract. Match on this field.
messagealwaysProse for a human. It can change in any release. Never match on it.
detailsoptionalThe bound that the request broke: {"field","limit","actual"}. It is omitted when the rejection carries no bound. It is never sent as {}

The HTTP status keeps its normal meaning. One code always answers with one status. The error reference lists every code, its status and the caller action for it.

Two failures are common on /info. An unknown type answers 400 with UNKNOWN_TYPE. An unknown named resource, such as a vault id, answers 404 with NOT_FOUND.

History-archive reads​

The history archive serves a group of reads. The node does not serve them. A success answers in the envelope above. A rejection does not.

The lane holds portfolio, historical_orders, user_funding, user_funding_by_time, user_position_history, user_position_history_by_time, user_non_funding_ledger_updates, recent_blocks, recent_transactions, validator_votes and user_volume_history.

portfolio takes an interval with address. The only accepted value is 1d. Any other value is rejected with 400 invalid interval: <value>, and the rejection names 1d.

A rejection on this lane puts a bare string in error. It does not send the {code, message} object. This applies to the lane reads that take an address. Two strings occur, both with status 400:

StringCause
missing field: addressThe request carries no address
invalid user address: <value>address is present but does not parse

A handler that branches on error.code reads undefined on this lane. Test the type of error before you read code.

Each read puts its rows under a key that names what they are. There is no generic data[]. Every key is snake_case, like the rest of this wire:

ReadCollection key
historical_ordersorders
user_funding, user_funding_by_timefundings
user_position_history, ..._by_timepositions
user_non_funding_ledger_updatesledger_updates
recent_transactionstxns
portfoliopoints
user_volume_historydays
warning

ledger_updates was ledgerUpdates. It was the one camelCase key on this wire. A client that reads data.ledgerUpdates gets undefined. Read data.ledger_updates.

warning

user_ledger and user_ledger_by_time are removed. Both were narrower views of the source that user_non_funding_ledger_updates serves. Its name states what it holds. Their names did not.

user_non_funding_ledger_updates returns every non-trading movement of an account's money, and nothing else. It excludes funding, because funding has its own read, user_funding. It excludes the realized PnL of a fill, because that is trading, and user_fills serves it.

Every row carries a kind and a signed delta. The sign of delta is taken from the side that the holder could spend a moment earlier. Money that leaves that side is negative. Money that arrives is positive. coin names the token. The kinds are:

kindWhat produced itExtra fields
depositA MetaBridge inbound credit, a system spot credit, or an EVM-to-Core creditchain on a bridge credit
withdrawA withdrawal to an EVM chain
transferAn account, sub-account or spot transfer, and a vault deposit or withdrawalcounterparty when the move has one
liquidationA forced-close settlement, or a delist settlementmarket, mark_px
staking_depositMTF moves from spot into the staking free pool
staking_withdrawMTF returns from the free pool to spot
delegateMTF moves from the free pool to a validator
undelegateMTF leaves a validator for the unbonding window
staking_rewardA claimed staking reward, credited to the free pool
earn_deposit / earn_withdrawAn Earn deposit or withdrawal

A vault movement is a transfer. It has no kind of its own. It carries no counterparty, because the other side is the vault.

market on a liquidation row is the perp market ID as a number. It is not a symbol. It is the only market reference on this read that the read does not resolve. Map it with markets_meta.

A forced close and a delist settlement write the same liquidation row. This read carries no cause to tell them apart. Read the cause from ledger_updates.

delegate and undelegate do not change the total that the account holds. They move MTF between the part that it can withdraw and the part that it cannot. This read shows that movement.

warning

Handle every kind in the table. A kind that your client does not know must never throw.

Membership of this lane is a deployment fact. The wire does not guarantee it. Do not hard-code the list. Write one handler that accepts both rejection shapes.

Malformed request body​

The two shapes above reject a request that the server could read. The server refuses a request that it cannot parse at an earlier step, and that answer carries no error key. Two forms occur, on every read:

BodyStatusCause
A bare JSON string, such as "Failed to deserialize the JSON body into the target type: ..."422Valid JSON, but a field holds the wrong type, for example limit as a string
Plain text, such as Failed to parse the request body as JSON: ...400The body is not valid JSON

Both messages are prose for a human, and they can change in any release. Never match on them. Check the status before you parse the body. Treat any 4xx whose body has no error object as a bug in your own request.

The server does not refuse every wrong type. A field that the read treats as optional, such as detail on account_state, falls back to its default and does not fail. Only a field with a declared numeric or typed binding rejects.

Empty results​

Inside a client, a read that answers nothing looks like a request that asks the wrong question. The first is a fact about the account. The second is a bug in your code. Three cases produce a zero that looks correct. Rule out each one before you report a holding as missing.

1. A retired type name. A name that this API no longer serves never answers with an empty body. It answers UNKNOWN_TYPE in one of two forms:

What you sentStatusBody
spot_clearinghouse_state, oracle_sources, sub_accounts, and every other name in removed reads that is not in the row below400{"error":{"code":"UNKNOWN_TYPE","message":"unknown info type: <name>"}}, with no details
account_overview, action_outcome, active_asset_ctx, all_mids, bridge_chain_configs, bridge_user_outbox, encode_action, evm_contract_bindings, gov_history, gov_proposals, gov_state, pm_summary, spot_meta, user_events, user_ledger_updates410The same code, plus details.use, which names the read to call instead. Before gateway 0.9.8, four of these names answered a bare 400: spot_meta, all_mids, active_asset_ctx and user_events. See relocated reads

So a 200 with an empty array is an answer about a real account. A client that shows "no balances" after it calls spot_clearinghouse_state has swallowed a 4xx. Check the status before you read the body.

2. A key that is not there. An absent key reads as undefined, and most code renders undefined as empty. The spot ledger is the common case. It is at data.spot.balances. An account_state body has no balances and no spot_balances at the top level. Both paths read as "this account holds nothing", and both are wrong. Where every field went lists every field that moved.

3. A source that the endpoint does not deploy. When no history archive is configured behind an endpoint, the archive-lane reads answer with a typed empty body, such as {"orders":[]} or {"fundings":[]}. On the wire, this is identical to an account with no history. The public endpoints run the archive, so this case applies only to a self-hosted endpoint.

To test your client, send the same request with a type that you know is wrong, such as "type":"nope". If your client reports the same empty result, it hides the error. It does not read an empty account.

Query types​

This table lists every type that the endpoint accepts, grouped by what it answers. Each link opens the request fields and the response schema of that type.

Grouptype
Account state
collateral, margin health, positions, reservations
account_state · clearinghouse_state
Orders & fills
resting orders, fill history, one order's lifecycle
open_orders · user_fills · order_status
Account history
funding payments, past orders, TWAP history
user_funding · user_volume_history · user_interest · historical_orders · action_outcome · user_twap_slice_fills · delegator_rewards
Perpetual markets
market metadata, books, trades, candles, funding
markets · markets_meta · l2_book · trades · candle_snapshot · funding_history · mip3_active_bids · liquidatable · active_asset_data · perp_dexs
Spot, margin & Earn
spot markets and balances, the margin lane, the lending pool
spot_meta · spot_margin_state · earn_state · user_interest · spot_deploy_auction
Position history
closed position lifecycles
user_position_history · user_position_history_by_time · identities
Options
the series registry and an account's open legs
option_series · option_state
Vaults & staking
vault TVL and share price, delegation state
vault_state · staking_state
Fees & credit
the fee card, the referral program, broker credit
fee_schedule · referral_state · referral_code · referral_referees · referral_leaderboard · broker_state
Governance
proposals, votes and the parameter set
validator_votes · gov_state · gov_proposals · gov_history
Chain activity
recent blocks, and one action's outcome
recent_blocks · recent_transactions
Node snapshots
peers, sync state and node-scoped figures
exchange_status · user_twaps · vault_summaries · user_rate_limit · approved_brokers · validator_l1_votes · validator_summaries · gossip_root_ips
Points
the weekly points table
points_weeks · points_leaderboard · points_user

Removed reads​

Each name in this section answers UNKNOWN_TYPE. The API keeps exactly one read for each question. Two reads for one question force a choice, and a wrong choice fails silently. For the release that removed each name, see migration.

The HTTP status tells the two kinds of removal apart:

  • 400: the name never named a read on this API, or its answer is gone.
  • 410: the name was public and its answer moved to another read. Fifteen names get this status: account_overview, action_outcome, active_asset_ctx, all_mids, bridge_chain_configs, bridge_user_outbox, encode_action, evm_contract_bindings, gov_history, gov_proposals, gov_state, pm_summary, spot_meta, user_events and user_ledger_updates. The error carries details.use, which names the read to call instead. A client can follow the move without this table. Branch on the status and on error.code. Never branch on the status alone.

details.use does not always name an /info type. action_outcome and encode_action both answer "use": "/exchange", which is an endpoint. Read the value as prose for a human. Do not post it back as a type.

RemovedCall this instead
abstraction_stateNothing. Its kind / value pair was free-form for each kind, so a value had no meaning that the wire defined
account_overview, web_dataaccount_state with detail: "overview". It returns the same body
action_outcomePOST /exchange. The submit call waits for the commit and returns the verdict. See its section
agentsaccount_state with detail: "overview", field agents
block_infoaccount_state for the committed height / time stamp. The archive-backed recent_blocks read for the block head. The explorer_block WS channel that answered this before is removed, because a validator must not serve a per-block firehose
bridge_chain_configsNothing. No public read carries the deployment row. A node publishes it on its node_bridge_outbox stream. The custody address for each chain is in the Deployments table
bridge_finalized_cosignatures, bridge_outbound_queuebridge_withdrawal_history for the withdrawals of one account. The queue of the whole chain and the raw validator cosignature bytes are not part of this API
delegator_historyNothing. No delegation event log is committed
delegator_summaryaccount_state with detail: "overview", field staking.summary
dynamic_riskmarkets_meta, field risk_override
encode_actionNothing. The multisig inner blob takes the ordinary {type, params} wire action. See signing the inner action
evm_contract_bindingsmarkets_meta with kind: "spot", field evm_contract
gov_state, gov_proposals, gov_historyvalidator_votes: status: "voting" for open votes, status: "enacted" for history. A parameter value is on the read that owns it: markets_meta, fee_schedule, exchange_status
leading_vaultsvault_summaries. Filter the rows on leader
margin_summaryaccount_state with detail: "margin"
market_infomarkets with coin, plus markets_meta with coin
max_builder_feeapproved_brokers. Find the builder in the list
max_market_order_ntls, perps_at_open_interest_capmarkets_meta. max_market_order_ntl is the served headroom, one row per market. null means uncapped. "0" means at the cap. Do not compute it from open_interest and oi_cap. An uncapped row omits oi_cap, so that arithmetic reads an uncapped market as zero headroom
node_infoNothing on this API. Per-node identity is not consensus state, so two honest nodes answer differently. The chain id is fixed for each network. See networks
oracle_sourcesNothing. The price aggregator does not read the per-market bitmask that it served. The static source facts are in prose. See oracle prices
perp_dex_limitsperp_dexs, field limits
pm_summaryaccount_state: perp.pm_maint_margin, perp.pm_concentration_penalty and the top-level pm_net_value. abstraction: "portfolio" is the enrolment flag
predicted_fundingsmarkets. The funding block of each row carries the charged rate and the next boundary
protocol_metricsmarkets, markets_meta and staking_state carry every public fact that it held. The rest was node diagnostics
recent_trades, trades_by_timetrades. Send no range for the recent window. Send a range for a time window
spot_clearinghouse_stateaccount_state. spot.balances is the whole token ledger
spot_deploy_statespot_deploy_auction. It is the same read with a new name
staking_aprstaking_state: pending_validator_pool_usdc and total_stake. It never served an APR
sub_accountsaccount_state with detail: "overview", field sub_accounts
token_infomarkets_meta with kind: "spot"
user_feesfee_schedule with address. It resolves the effective maker / taker bps
user_fills_by_timeuser_fills with start_time / end_time
user_ledger_updatesuser_non_funding_ledger_updates. The node keeps no per-account ledger history. The archive keeps every balance movement in the record shape of the stream, with a signed delta and a token coin. That read serves it. One question has one read. The gateway answers 410 with details.use. A node that you call directly answers 400 UNKNOWN_TYPE. Before block 25,599,540, the name answered 200 with updates: []
user_roleaccount_state with detail: "overview", field role
user_to_multi_sig_signersaccount_state with detail: "overview", field multisig
user_vault_equitiesaccount_state with detail: "overview", field vault.equities
web_data2account_state for margin and balances, clearinghouse_state for positions, detail: "overview" for vault equities, open_orders for resting orders, exchange_status for status. The WS channel is removed too. It answers {"channel":"error","data":{"error":"unknown channel: web_data2"}}

Capability-gated reads​

These two reads exist on the wire. Each one answers with the error that an unknown type gets. The cause is that the capability it reads is not reachable yet. The read is not restricted to operators. Each read becomes public on the day that its capability becomes reachable.

ReadShips when
mip3_deployer_oracleThe mip3_deployer_oracle protocol feature is armed on the target chain
fba_batch_stateThe FBA engine becomes reachable from /exchange

Points reads​

info

Served since gateway 0.9.17, and no week is published yet. Each read answers with empty rows until the first week is published. The archive publishes weeks only after the operator starts the program. Points still count from genesis. When publication starts, the archive publishes every past week. See block 34,135,466.

The points program publishes one table a week. The history archive computes it, and these three reads serve it.

Amounts are decimal strings in whole units: USD for a volume, points for a point count. They carry up to six decimals. Week numbers, season numbers, blocks and times are numbers. A time is in milliseconds.

Weeks count from genesis. Week 1 ends 2026-09-09 00:00 UTC. Each later week ends seven days after the week before it, at a Wednesday 00:00 UTC cut. A week argument is this number. It is not a week number inside a season.

A rejected argument answers 400 with the message missing field: season, missing field: address or invalid user address: <value>. Test the type of error before you read code, as on the archive lane.

With no archive behind the endpoint, each read answers 200 with rows: [] and a flag string. The public endpoint runs the archive.

points_weeks​

Returns every published week, oldest first. A published week never changes, so its two hashes never change.

Request

{ "type": "points_weeks" }

Response

{
"data": {
"type": "points_weeks",
"rows": [
{
"week": 5,
"season": 1,
"week_start": 1790726400000,
"week_end": 1791331200000,
"block_from": 23140512,
"block_to": 28617903,
"gap_blocks": 0,
"pool": "1000000",
"q_floor": "50000000",
"total_qualifying_volume": "8000000",
"issued_points": "160000",
"table_sha256": "a5c941fbd48fb9774d4938a4cd625f345a324562f2e19c3d80e8ae84f492828e",
"inputs_sha256": "3103ca2c526671100cddc82b201afff8768a8c39b2dafefc6f0c5ccc9cc66441",
"published_at": 1791332400000
}
]
}
}
FieldMeaning
weekWeek number since genesis
seasonSeason number, 1 to 4
week_start / week_endThe start of the week (inclusive) and its cut (exclusive)
block_from / block_toThe first block of the week, and the cut block: the last block before week_end
gap_blocksBlocks of the range where the archive has a recorded gap in fills. The week counts the fills that the archive holds, and the pool does not change
poolPoints that the week shares. "1000000", except the partial last week before the TGE
q_floorQ_floor, in USD
total_qualifying_volumeΣqv over every root, in USD
issued_pointsPoints that the week issued. Below pool when Σqv is below q_floor
table_sha256SHA-256 of the full table. See check the table hash
inputs_sha256SHA-256 of the exclusion, root and cluster inputs of the week. It commits to them and does not show them
published_atWhen the archive published the week

points_leaderboard​

Returns the full table of one week, or the season total. Rows are ranked by points, highest first, then by address. A page holds 1,000 rows.

Request

{ "type": "points_leaderboard", "season": 1, "week": 5, "offset": 0 }
FieldTypeRequiredMeaning
seasonnumberyesSeason number
weeknumbernoWeek number since genesis. Omit it for the season total. A week outside season answers no rows
offsetnumbernoRows to skip. Default 0. Add 1,000 for the next page

Response

{
"data": {
"type": "points_leaderboard",
"season": 1,
"week": 5,
"offset": 0,
"rows": [
{
"rank": 1,
"address": "0x1933587ffa064e26bf8f6c7b0fdec771644b56de",
"raw_volume": "5350000",
"qualifying_volume": "5350000",
"points": "107000"
},
{
"rank": 2,
"address": "0xac7dc3a2f08610ddc46c7a3e4f54b80745cbd8e9",
"raw_volume": "3000000",
"qualifying_volume": "2650000",
"points": "53000"
}
]
}
}
FieldMeaning
weekThe week that you asked for, or null for the season total
rankoffset plus the place of the row on the page, from 1
addressThe root account
raw_volume / qualifying_volumeraw and qv, in USD
pointsPoints for the week, or for the season

The table holds every root that has raw volume, including roots that earned no points. A root under the $1,000 minimum has a row with points "0". Excluded accounts have no row. A page with fewer than 1,000 rows is the last page.

points_user​

Returns the points of one account, one row per week that it traded.

Request

{ "type": "points_user", "address": "0xdaca1e722d0a355b2242f144b3dfb1cc378e9405", "season": 1 }
FieldTypeRequiredMeaning
addressaddressyesAny account. A sub-account or user vault answers with the rows of its root
seasonnumbernoSeason number. Omit it for every season

Response

{
"data": {
"type": "points_user",
"address": "0xdaca1e722d0a355b2242f144b3dfb1cc378e9405",
"root": "0xac7dc3a2f08610ddc46c7a3e4f54b80745cbd8e9",
"season": 1,
"rows": [
{
"week": 5,
"season": 1,
"week_end": 1791331200000,
"raw_volume": "3000000",
"qualifying_volume": "2650000",
"points": "53000"
}
]
}
}
FieldMeaning
addressThe address that you sent, in lower case
rootThe account that earns for address. It differs from address for a sub-account or a user vault
seasonThe season that you asked for, or null for every season
rowsThe weeks of the root, oldest first

The rows belong to the root. A sub-account and its parent answer the same rows.

An empty rows has three possible causes: the account had no raw volume, no week is published yet, or the account is excluded. The read does not tell them apart.

Errors​

The error reference has the full list, with the caller action for each code. /info produces these codes:

HTTPerror.codeCause
200—Success. An unknown address on account_state and its siblings answers 200 with a zeroed record. It is not a 404
400INVALID_REQUESTNo type discriminator, a required type-specific argument is missing, or an address is malformed
400UNKNOWN_TYPEThe type names no read. It is misspelled, or the read is removed
410UNKNOWN_TYPEThe type names a read whose answer moved. details.use names the read to call instead. See removed reads
404MARKET_NOT_FOUNDThe coin symbol is unknown (markets, l2_book and other market reads)
404NOT_FOUNDA named resource is unknown, such as a vault address on vault_state
429RATE_LIMITEDNo retry hint is sent. See rate limits
500INTERNALA server defect. Your request is not the cause. Retry, then report it

A 405 carries no envelope. The endpoint is POST-only, and the router refuses another method before the envelope exists.

warning

There is no account not found error. Account-keyed reads, such as account_state, open_orders, user_rate_limit and staking_state, return a 200 zeroed record for an address that never appeared on-chain. They never answer 404.

Read-after-write consistency​

/info reads from the most recent committed block. A POST /exchange admitted at time T becomes visible in /info when the leader commits the block that contains it. That is one committed block later. Block cadence is a governed target for each deployment. It is not a fixed duration. For a wall-clock estimate, measure the committed-round rate of your deployment.

To read your own writes, subscribe to order_updates for the order lifecycle and fills for executions. Committed events arrive in commit order, so you do not need to poll.

Order visibility sequence​

See also​

FAQ​

Show FAQ

Q: Do I address a market by id or by name? A: By coin symbol ("BTC"). The numeric asset_id / market_id request arguments are removed. Only coin is accepted, and responses show coin symbols everywhere. The signed /exchange write path still uses the numeric asset. That field is consensus-frozen, and it is not related to these read arguments.

Q: Do user_fills / trades need an external indexer? A: No. On the gateway, since block 25,599,540, the node keeps no committed fill ring and no trade ring. The gateway answers both reads from the archive and from its own 24-hour trade window. A bare node answers trades with "trades": []. For a continuous real-time feed, subscribe to the WS channels. See deep history past the ring.

Q: Is the response deterministic across nodes? A: Yes. Every honest node returns identical responses for the same query at the same committed height. Nodes at different commit heights can differ. Compare the height / time stamp that account_state carries before you call two answers inconsistent. gossip_root_ips is the one field that is not consensus state. It reads the config of each node. Nodes that carry the same roster answer identically, and nodes that do not can differ.