Skip to main content

Account & access actions

Actions on POST /exchange. The request envelope, the EIP-712 signing rules, the number planes and the response shape are on that page and apply to every action here.

Approve an agent wallet

Approve an agent wallet to sign on the account's behalf. See agent wallets for the lifecycle.

{
"type": "approve_agent",
"params": {
"agent": "0x00000000000000000000000000000000000000aa",
"name": "trading-bot-1",
"expires_at_ms": 1735689600000
}
}
FieldTypeDescription
agenthex address20-byte address of the agent's signing key
namestring | nullOptional bookkeeping label
expires_at_msuint64 | nullUnix-ms expiry; null = never expires

Response. Non-order action → 202 Accepted admission envelope:

{ "data": { "accepted": true, "mempool_depth": 1, "nonce": 1735689600001, "action_hash": "0x..." } }

There is no synchronous approval confirmation in the HTTP body — track the commit via the returned action_hash.

Common errors (at commit): cannot approve self (the agent address equals the sender), zero address. Re-approving an already-approved agent overwrites its entry (name + expires_at_ms) rather than erroring.

Becomes effective one block after commit. Submitting an agent-signed action before then returns 401.


Set the account display name

Set the account's human-readable handle.

{
"type": "set_display_name",
"params": { "display_name": "alice.mtf" }
}
FieldTypeDescription
display_namestringThe handle (e.g. alice.mtf)

Bind the account to a referrer

Bind the account to a referrer address (not a code).

{
"type": "set_referrer",
"params": { "referrer": "0x00000000000000000000000000000000000000bb" }
}
FieldTypeDescription
referrerhex address20-byte referrer address

Settable once per account. A later attempt is refused with PRECONDITION_FAILED, whose message names the reason.


Approve a broker fee ceiling

Approve a broker address up to a fee ceiling (bps). 0 revokes; the core handler caps at 8 bps.

{
"type": "approve_broker_fee",
"params": {
"builder": "0x00000000000000000000000000000000000000aa",
"max_bps": 7
}
}
FieldTypeDescription
builderhex address20-byte broker address. The field keeps the builder name
max_bpsuint16Max approved fee in bps (0 revokes; capped at 8)
note

Both action types are accepted. approve_broker_fee is the name to send. approve_builder_fee still decodes and always will: a committed block keeps the JSON the trader submitted, and replay reads it again. The EIP-712 type string stays ApproveBuilderFee, which no signature lets you change — see broker codes.


Claim accrued referral credit

Drain the sender's whole accrued referral credit into spendable cross-collateral. No parameters.

{ "type": "claim_referral_rewards", "params": {} }

The action reports no amount. Read the balance first with referral_state. After the claim, the credit is 0 and the read no longer tells you what moved.

An agent wallet cannot claim for its owner. The action is sender-authorized and carries no owner field, so it always acts on the recovered signer's own account. Sign it with the master key.

The call is idempotent. Claiming with nothing accrued claims 0 and is not an error, so a retry after a timeout is safe.


Claim accrued broker-code credit

Drain the sender's whole accrued broker-code fee credit into spendable cross-collateral. No parameters.

{ "type": "claim_broker_rewards", "params": {} }

Both names are accepted. claim_broker_rewards is the name to send. claim_builder_rewards still decodes and always will, for the same reason approve_broker_fee keeps its second name. The read beside it keeps the builder spelling and is broker_state.

The action reports no amount. Read the balance first with broker_state.

An agent wallet cannot claim for its owner. The action is sender-authorized and carries no owner field, so it always acts on the recovered signer's own account. Sign it with the master key.

The call is idempotent. Claiming with nothing accrued claims 0 and is not an error. See broker codes.


Convert the account to multi-sig

Register a multi-sig roster on the account. It takes effect at that commit.

{
"type": "convert_to_multi_sig_user",
"params": {
"signers": [
"0x00000000000000000000000000000000000000aa",
"0x00000000000000000000000000000000000000bb"
],
"threshold": 2
}
}
FieldTypeDescription
signersarray of hex addressesThe multi-sig signer set
thresholduint32M-of-N threshold: at least 1, and no more than the number of signers. The one exception is the disable form below, which pairs an empty signers with threshold: 0
Only the roster can change the roster

From that commit on, a plain signed action from the account is refused. The account acts only through the multi_sig wrapper below — including when it re-keys or turns multi-sig off. Both are the same action wrapped in a multi_sig envelope that the current roster signs:

  • Re-key: a new signers + threshold. It replaces the roster.
  • Disable: signers: [] with threshold: 0. It removes the roster and returns the account to its single key.

So register a roster you can still reach a quorum on. If you lose the quorum, you lose the account, because nothing outside the roster can repair it.

See multi-sig.


Execute an inner action as a multi-sig account

This is the collect-and-execute wrapper. It is the only way a converted account acts. It carries ONE inner action as opaque bytes, plus the roster signatures that authorize it, and it runs that inner action as user. Register the roster first with convert_to_multi_sig_user; see multi-sig for the worked flow and the inner digest.

Anyone may submit it. The outer envelope is signed by the submitter with the submitter's own key, and the submitter need not be on the roster — a coordinator, or any one signer, can broadcast the assembled bundle. All authority comes from the recovered addresses in signatures.

Its EIP-712 typed-data primary type is MetaFluxTransaction:MultiSig.

{
"type": "multi_sig",
"params": {
"user": "0x00000000000000000000000000000000000004a1",
"inner_action_blob": "0x7b2274797065223a2263616e63656c5f616c6c5f6f7264657273227d",
"signatures": ["0x<65-byte sig>", "0x<65-byte sig>"],
"nonce": 1735689600099
}
}
FieldTypeRange / valuesDescription
userhex address40 hex charsThe multi-sig account. It must already carry a registered roster
inner_action_blobhex string0x-hex, non-emptyThe canonical JSON bytes of the inner action. These EXACT bytes are what every signer signs and what the server hashes. They are never re-serialized
signaturesarray of hex stringseach 65 bytesRoster signatures over the inner digest. There is no per-entry signer field — the signer is recovered
nonceuint64The inner nonce. It advances user's nonce window, and every signer folds it into the inner digest

There is no signers field on the wire. A declared list would not add authority: the roster membership is what counts, and it is recovered, not declared.

The two nonces are checked against two different accounts. params.nonce advances user's window inside the handler. The envelope's own top-level nonce advances the submitter's window before dispatch. Setting them equal is the convention, and it is what the worked flow shows, but the chain does not compare them.

The rules, written as rejections.

The callResult
user has no registered roster, or a zero thresholdRejecteduser not multi-sig / multi-sig threshold zero
An empty inner_action_blobRejected, InvalidParamsempty inner_action_blob
A signature of the wrong length, or from a non-roster keySilently skipped. One malformed entry must not block an otherwise valid quorum, so it is not an error — it does not count
Fewer than threshold distinct roster signers recoveredRejected, AUTH_UNAUTHORIZED
A stale or replayed params.nonceRejectedstale or replayed multi-sig nonce
An inner action outside the executable set: a governance vote, a system write, or a nested multi_sigRejected, AUTH_UNAUTHORIZED — and user's nonce has already advanced. This is deliberate: a valid quorum cannot retry the same nonce with a privileged body swapped in
An inner blob whose own owner field names an account other than userRejected, InvalidParamsinner_action_blob owner must be the multisig user

The nonce advances the moment the quorum verifies, and before the blob is even decoded. So a bad-signature attempt never burns user's nonce, and EVERY failure after a valid quorum does — an undecodable blob, the owner mismatch, a non-executable inner, and an inner that fails its own handler.

That is deliberate: a valid quorum must not be able to retry one nonce with a different body. The consequence for a caller is the part to remember. After any of those refusals, re-sign with the NEXT nonce. Retrying the same one gets stale or replayed multi-sig nonce, which reads like a replay guard firing on a request that was never accepted.

Every authorization failure reads the same

AUTH_UNAUTHORIZED carries the flat message "unauthorized". It does not say which check failed: threshold not met, signer not in the roster, and inner action not executable all answer with that one string. Diagnose by re-deriving the inner digest and the recovered addresses on your own side.

The inner action runs as user, so every rule that applies to user posting it directly applies here too, including its own margin and balance gates.


Create a sub-account

Open a sub-account owned by the sender (the recovered signer becomes the sole master). The sub-account gets a derived on-chain address that carries its own balances. Sender-authorized — no owner field.

{
"type": "create_sub_account",
"params": {
"name": "trading-bot-1",
"explicit_index": null,
"shared_stp_group": true
}
}
FieldTypeDescription
namestringHuman-readable label for the sub-account (non-empty)
explicit_indexuint32 | nullOptional explicit sub-account index; null = use the next free index. An in-use explicit index is rejected at commit (index in use)
shared_stp_groupboolWhether the sub-account shares the parent's self-trade-prevention group

Response. Non-order action → 202 Accepted admission envelope. The assigned sub_id and derived sub-account address are carried in the commit outcome, not the HTTP body — track the commit via the returned action_hash.

Common errors (at commit): empty name, index in use.


Transfer collateral between master and sub-account

Move perp cross-margin USDC collateral between the master account and one of its sub-accounts. Sender-authorized — no owner field; the signer is the master.

{
"type": "sub_account_transfer",
"params": {
"sub_index": 0,
"deposit": true,
"amount": "150.5"
}
}
FieldTypeDescription
sub_indexuint32Index of the sender's sub-account (as assigned at create time)
depositbooltrue = master → sub; false = sub → master
amountdecimal stringCross-margin USDC to move (> 0), as a JSON string

The source must hold at least amount of free cross-collateral; debit + credit are equal so the parent-plus-subs total is conserved.

Response. Non-order action → 202 Accepted admission envelope.

Common errors (at commit): amount must be positive, sub account not found (unknown/unowned sub_index), insufficient cross collateral.


Transfer spot tokens between master and sub-account

Move a spot token balance between the master account and one of its sub-accounts. Sender-authorized — no owner field.

{
"type": "sub_account_spot_transfer",
"params": {
"sub_index": 0,
"token": 101,
"deposit": false,
"amount": "42"
}
}
FieldTypeDescription
sub_indexuint32Index of the sender's sub-account
tokenuint32Spot token id to move
depositbooltrue = master → sub; false = sub → master
amountdecimal stringToken amount to move (> 0), as a JSON string

The source must hold at least amount of the token; the per-token parent-plus-sub total is conserved.

Response. Non-order action → 202 Accepted admission envelope.

Common errors (at commit): amount must be positive, sub account not found, insufficient spot balance. A split standard leg moves USDC through its spot wallet, so on that leg the USDC rejection is insufficient spot balance, not insufficient cross collateral; a pooled leg keeps insufficient cross collateral.


Toggle one-way vs hedge position mode

Toggle the account between one-way (single net position per market) and hedge mode (a separate long leg and short leg per market). Sender-authorized by default — omit owner and the recovered signer is the actor; an approved agent may toggle it as an owner it acts for.

{
"type": "set_position_mode",
"params": { "hedge": true }
}
FieldTypeValuesDescription
ownerhex address | omitted40 hex charsOptional: toggle as this account (approved agents only). Not digest-bound — resolved at admission
hedgebooltrue / falsetrue = hedge (two-way), false = one-way (the default)

Precondition — flat on all markets. The toggle is only legal when the sender holds no open position on any market (every leg flat). If any position is open, the action is rejected as a clean no-op (state is left byte-identical): this prevents an existing net position from being silently re-interpreted as a stranded leg. Setting the mode to the value it already has, while flat, is a no-op success.

Common errors: precondition failed: cannot change position mode with an open position (the account is not flat).

info

Once an account is in hedge mode, every order must carry an explicit position_side ("long" / "short") — see position_side on submit_order. Per-leg margin / liquidation and dual-leg position reporting are still rolling out; see hedge mode for the current availability.


Toggle DEX-abstraction for the account — removed

danger

Removed. Do not send this action. user_dex_abstraction was deleted at the 0.7.0 re-genesis and has no handler. A submit returns 400 with ACTION_UNSUPPORTED.

MetaFlux runs one unified account with portfolio margin, so there are no separate DEXes to abstract over. There is no replacement action and none is planned. The action id stays permanently reserved and is never reused.


Set the account's margin mode and per-product reservations

Chooses the account mode, and — on a pooled standard account — how much USDC each product may encumber.

{
"type": "user_set_abstraction",
"params": { "kind": 0, "value": "1" }
}
FieldTypeDescription
kinduint80 sets the mode; 1 perp, 2 spot, 3 option reservation. Any other value is rejected. Kinds 1–3 apply to a pooled standard account only.
valuedecimal (string or number)For kind: 0, 0 = unified or 1 = standard. For a reservation, whole USDC; 0 removes it.

Modes. unified is the default: one USDC balance, and any product may draw on all of it. standard holds two USDC wallets, a perp wallet and a spot wallet, and only usd_class_transfer moves USDC between them. A standard account that entered below block 5,710,001 is pooled (split: false on account_state): it keeps one balance, and reservations divide it by product.

A reservation is a CEILING ON ENCUMBRANCE, not on spending. Reservations exist on a pooled account only. This is the rule callers get wrong, so read it before you set one. A reservation caps how much USDC a product may have COMMITTED at one time — perp margin, an option writer's escrow, a spot-margin borrow. It does not cap what a product may SPEND. An option PREMIUM and a plain spot BUY are conversions, not encumbrance: the USDC leaves the account and something else arrives, so they are bounded by your balance, never by a reservation. Only the escrow the option WRITER posts is bounded by the option reservation.

A pooled account admits nothing its reservations do not cover. An unset reservation is zero, and zero admits nothing. That is deliberate and fail-closed.

A split account has no reservations. Its perp and option orders are admitted against the perp wallet's free collateral, and its spot orders against the spot wallet, with no cap. Kinds 1–3 on a split account are refused, whatever the value.

Not live yet

Uncapped admission and the refusal of kinds 1–3 on a split account ship with the next node release after 0.9.7. Until then, a live node refuses only a nonzero kind: 2 on a split account. It caps the perp and option orders of a split account by the perp and option reservations, so a split account with no perp reservation opens no perp position.

A mode change needs a FLAT account. Every perp leg, spot order, spot-margin position, option position, live TWAP, parked trigger and open RFQ must be gone. The rejection names the first surface it found. A RESERVATION change needs no flat account — but lowering one below what is already committed does not release anything, it only stops further commitment. On a pooled account, lowering a reservation is always allowed, even when your equity has fallen below the total already reserved.

standard and portfolio are mutually exclusive. Each refuses the other, in both directions.

The USDC split. From node 0.9.7 (block 5,710,001), an account that ENTERS standard also splits its USDC: all of it stays in the perp wallet, the spot wallet starts empty, and only usd_class_transfer crosses. Entry is refused while the perp wallet is below zero. An account already in standard at the swap is NOT split until it leaves and re-enters. Leaving folds the spot wallet back into the pool, and is refused while that wallet is below zero. See the standard-mode split.

Rejections, all Precondition unless noted:

MessageCause
unknown abstraction kind (InvalidParams)kind above 3
abstraction mode must be 0 (unified) or 1 (standard) (InvalidParams)a kind: 0 value that is neither
reservation must be >= 0 (InvalidParams)a negative reservation
reservations require standard abstraction modea reservation set on a unified account
reservations exceed account valuean INCREASE whose new total exceeds account value
cannot change abstraction while enrolled in portfolio marginPM enrolled
cannot change abstraction with <surface>the account is not flat
a split standard account has no reservationskind 1, 2 or 3, any value, on a split standard account. Not live yet: a live node refuses only a nonzero kind: 2, with spot has its own wallet in standard mode; no spot reservation
perp wallet is negative; cannot enter standard modekind: 0, value: 1 while the perp wallet is below zero
spot wallet is negative; cannot leave standard modekind: 0, value: 0 while the split account's spot wallet is below zero

Set another user's abstraction config

danger

agent_set_abstraction is not available, and it never worked. This page described a working action. That was wrong: the handler accepted the call and wrote nothing, so the config it named never changed. The correction is the whole rule — there is no version of this action that sets a config.

Every call is now refused with PRECONDITION_FAILED and the message agentSetAbstraction is not available; the account owner must sign userSetAbstraction. The refusal does not depend on the sender, the target account, the kind, or on the sender being an approved agent of params.user.

Why. An approved agent holds trading authority only — place, cancel, modify, and tune position risk on the owner's own account. The abstraction mode is not a trading setting. Leaving standard mode moves the account's spot wallet into its perp wallet, and a reservation refuses the owner's own later orders. Both are owner-only, so the owner signs user_set_abstraction from the master key.

The action stays on the wire and keeps its type and its EIP-712 type string. It never succeeds.

Not live yet: the refusal ships with the next node release. A live node answers a 202 and commits nothing.

The request shape below is what the action still accepts on the wire.

{
"type": "agent_set_abstraction",
"params": {
"user": "0x00000000000000000000000000000000000000bb",
"kind": 1,
"value": "9.9"
}
}
FieldTypeDescription
userhex addressThe user whose config the agent is updating
kinduint8Sub-type tag
valuedecimal (string or number)Setting value