Skip to main content

POST /info — read & query endpoint

info

Status. stable shape. Query types are added over time; the envelope is committed.

info

One name for the account: address. Every account-scoped query takes it. Four queries shipped with user instead — broker_state, referral_state, spot_margin_state and user_interest. Those still accept user, and their replies carry the account under both names. Send address in new code.

TL;DR

Single endpoint, multi-type. Dispatches on the request body's type field. Read-only — never mutates state, never requires a signature.

tip

Split by product. Perp-market read queries are on perpetual queries; spot, spot-margin, and Earn read queries are on spot & margin queries; closed-position lifecycle queries are on position history; governance queries are on governance queries. This page covers the envelope, conventions, and account/vault/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. Running the node yourself, the same native /info is served directly 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 */ }

Success200 OK. The type discriminator is echoed INSIDE data:

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

The payload fields keep their old path. A field 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, the history-archive reads included. The archive lane differs from the envelope above in one place only: the shape of a rejection. See the archive lane below.

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

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

Failure — no data key, and the HTTP status that the code maps to:

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

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

Two common /info failures: an unknown type answers 400 with UNKNOWN_TYPE; an unknown named resource, such as a vault id, answers 404 with NOT_FOUND.

The history-archive reads reject with a bare string

A group of reads is served by the history archive rather than by the node. A success answers in the envelope above. A rejection does not.

The lane is 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 alongside address, and it accepts exactly one value: 1d. Any other value is rejected 400 invalid interval: <value>, and the rejection now names 1d.

A rejection puts a bare STRING in error, not the {code, message} object. This applies to the reads in the lane 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.

The collection key, per read. The rows sit under a key named for what they are, not under a 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

⚠️ ledger_updates was ledgerUpdates. It was the one camelCase key on this wire. A client reading data.ledgerUpdates now gets undefined — read data.ledger_updates.

⚠️ user_ledger and user_ledger_by_time are REMOVED. Both were narrower views of the same source that user_non_funding_ledger_updates already serves, and its name states what it holds where theirs did not.

What user_non_funding_ledger_updates means. Every NON-TRADING movement of an account's money, and nothing else. Funding is excluded because it has its own read, user_funding; a fill's realized PnL is excluded because it is trading, and it already has user_fills.

Every row carries a kind and a signed delta. delta is signed from the side the holder could spend a moment earlier: money that leaves that side is negative, money that arrives is positive. coin names the token. These are the kinds:

kindWhat produced itExtra fields
depositA MetaBridge inbound credit, a system spot credit, or an EVM→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, not a 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, not a symbol. It is the one market reference on this read that is not resolved for you. Map it with markets_meta.

A forced close and a delist settlement write the same liquidation row, and this read carries no cause to tell them apart. Read the cause from ledger_updates. NOT LIVE YET: a live node settles no position at a delist.

delegate and undelegate do not change what the account holds in total. They move MTF between what it can withdraw and what it cannot, and that is a movement this read must show.

⚠️ Handle the full table. A kind your client does not know must never throw.

Membership of this lane is a deployment fact, not a wire guarantee. Do not hard-code the list; write one handler that accepts both rejection shapes.

A malformed request body answers with no error key at all

The two shapes above both reject a request the server could read. A request the server cannot even parse is refused earlier, and that answer carries no error key. Two forms occur, on every read alike:

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 can change in any release. Never match on them. Parse the body only after you check the status, and treat any 4xx whose body has no error object as a bug in your own request.

Not every wrong type is refused. A field the read treats as optional, such as detail on account_state, falls back to its default rather than failing. Only a field with a declared numeric or typed binding rejects.

Empty is not the same as absent

A read that answers nothing and a request that asks the wrong question look alike inside a client. The first is a fact about the account. The second is a bug in your code. Three cases produce a plausible-looking zero — rule each one out before you report a holding as missing.

1. You sent a retired type name. A name 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>"}} — no details
account_overview, action_outcome, bridge_chain_configs, bridge_user_outbox, encode_action, evm_contract_bindings, gov_history, gov_proposals, gov_state, pm_summary410the same code, plus details.use naming the read to call instead
spot_meta, all_mids, active_asset_ctx, user_events400 today, 410 at the next node releaseNot live yet. These four answer a bare 400 on the running chain even though this reference names a replacement for each. They move to 410 with details.use at the next release — see next release

So a 200 carrying an empty array IS an answer about a real account. A client that shows "no balances" after calling spot_clearinghouse_state swallowed a 4xx. Check the status before you read the body.

2. You read a key that is not there. An absent key reads as undefined, and undefined renders as empty almost everywhere. The spot ledger is the one that catches people. It lives at data.spot.balances. There is no balances and no spot_balances at the top level of an account_state body; both paths read as "this account holds nothing", and both are wrong. Every field that moved is listed in where every field went.

3. The read's source is not deployed on the endpoint you called. The archive-lane reads answer with a typed empty body — {"orders":[]}, {"fundings":[]} — when no history archive is configured behind that endpoint. On the wire that is identical to an account with no history. The public endpoints run the archive, so this is a self-hosted concern.

The one-line control. Send the same request with a type you KNOW is wrong, such as "type":"nope". If your client reports the same empty result it reported before, it is hiding the error, not reading an empty account.

Query types

Every type the read endpoint accepts, grouped by what it answers. Click a type for its request fields and response schema.

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
ledger updates, funding payments, TWAP history
user_funding · user_volume_history · user_interest · user_ledger_updates · 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, referral and broker credit
fee_schedule · referral_state · 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

Removed reads

Each name here answers UNKNOWN_TYPE. The read surface is cut so that each question has exactly one read: two reads for one question force a choice, and a wrong choice is silent. For the release a removal landed in, see migration.

The status splits the two kinds of removal:

  • 400 — the name never named a read on this API, or its answer is gone.
  • 410 — the name was public and its answer MOVED. Ten names get this today: account_overview, action_outcome, bridge_chain_configs, bridge_user_outbox, encode_action, evm_contract_bindings, gov_history, gov_proposals, gov_state and pm_summary. The error carries details.use, naming the read to call instead, so a client can follow the move without reading this table. Four more join them at the next node releasespot_meta, all_mids, active_asset_ctx and user_events answer a bare 400 until then, which is wrong for a name this reference gives a replacement for. Branch on the status AND on error.code, never on the status alone.

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

RemovedCall this instead
abstraction_stateNothing. Its kind / value pair was per-kind free-form, so a value had no wire-defined meaning
account_overview, web_dataaccount_state with detail: "overview" — the same body
action_outcomePOST /exchange — the submit call already waits for the commit and returns the verdict. See the section above
agentsaccount_state with detail: "overview"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 used to answer this is removed — 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, and the custody address per chain is in the Deployments table
bridge_finalized_cosignatures, bridge_outbound_queuebridge_withdrawal_history for one account's own withdrawals. The whole-chain queue 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"staking.summary
dynamic_riskmarkets_metarisk_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"evm_contract
gov_state, gov_proposals, gov_historyvalidator_votesstatus: "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 — look the builder up in the list
max_market_order_ntls, perps_at_open_interest_capmarkets_metamax_market_order_ntl is the served headroom, one row per market. null = uncapped, "0" = at the cap. Do not rebuild it from open_interest and oi_cap: an uncapped row OMITS oi_cap, so that arithmetic reads uncapped 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 per network — see networks
oracle_sourcesNothing. The per-market bitmask it served is not read by the price aggregator. The static source facts are prose — see oracle prices
perp_dex_limitsperp_dexslimits
pm_summaryaccount_stateperp.pm_maint_margin, perp.pm_concentration_penalty and the top-level pm_net_value, with abstraction: "portfolio" as the enrolment flag
predicted_fundingsmarkets — each row's funding block carries the charged rate and the next boundary
protocol_metricsmarkets, markets_meta and staking_state carry every public fact it held. The rest was node diagnostics
recent_trades, trades_by_timetrades — un-ranged for the recent window, ranged for a time window
spot_clearinghouse_stateaccount_statespot.balances is the whole token ledger
spot_deploy_statespot_deploy_auction — the same read, renamed
staking_aprstaking_statepending_validator_pool_usdc and total_stake. It never served an APR
sub_accountsaccount_state with detail: "overview"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_roleaccount_state with detail: "overview"role
user_to_multi_sig_signersaccount_state with detail: "overview"multisig
user_vault_equitiesaccount_state with detail: "overview"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, and answers {"channel":"error","data":{"error":"unknown channel: web_data2"}}

Reads gated by their capability

These two reads exist on the wire already. Each answers with the same error an unknown type gets, not because it is restricted to an operator, but because the capability it reads is not reachable yet. Each ships publicly the day that capability does.

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

Errors

Read the full list, with the caller action for each code, in the error reference. These are the codes /info produces:

HTTPerror.codeCause
200Success. An unknown address on account_state and its siblings is a 200 with a zeroed record, NOT a 404
400INVALID_REQUESTNo type discriminator, a required type-specific argument omitted, or a malformed address
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
500INTERNALOur defect, not your request. 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 readers (account_state, open_orders, user_rate_limit, staking_state, …) return a 200 zeroed record for an address that has never appeared on-chain — they never 404.

Read-after-write consistency

/info reads from the most recent committed block. A POST /exchange admitted at time T is not visible in /info until the leader commits the block containing it — one committed block later. Block cadence is a governed, per-deployment target, not a fixed duration; measure your own deployment's committed-round rate if you need a wall-clock estimate.

For read-your-writes semantics, subscribe to order_updates (order lifecycle) and fills (executions); committed events arrive in commit order, removing the need to poll.

Sequence — query an account, see your own order

See also

FAQ

Show FAQ

Q: How do I address a market — by id or by name? A: By coin symbol ("BTC"). The legacy numeric asset_id / market_id request arguments were removed; only coin is accepted, and responses render coin symbols everywhere. (The signed /exchange write path still uses the numeric asset — that field is consensus-frozen and unrelated to these read args.)

Q: Do user_fills / trades need an external indexer? A: No. Both read a committed on-node tape (a bounded per-account fill ring and per-market trade ring folded into the AppHash), so any node serves real records directly — no external indexer required. The rings are bounded, so they hold a recent window; for an unbroken live feed subscribe to the WS channels. History PAST the ring is a different question: the archive holds it, and a RANGED ask (one that carries start_time) reaches it. An un-ranged ask always answers from the ring — see Deep history, past the ring.

Q: Is the response deterministic across nodes? A: Yes. Any honest node returns identical responses for the same query at the same committed height. Nodes with different commit heights may differ, so compare the height / time stamp account_state carries before you call two answers inconsistent. gossip_root_ips is the one field that is NOT consensus state: it reads each node's own config, so nodes that carry the same roster answer identically, and nodes that do not may differ.