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
}
}
| Field | Type | Description |
|---|---|---|
agent | hex address | 20-byte address of the agent's signing key |
name | string | null | Optional bookkeeping label |
expires_at_ms | uint64 | null | Unix-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" }
}
| Field | Type | Description |
|---|---|---|
display_name | string | The 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" }
}
| Field | Type | Description |
|---|---|---|
referrer | hex address | 20-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
}
}
| Field | Type | Description |
|---|---|---|
builder | hex address | 20-byte broker address. The field keeps the builder name |
max_bps | uint16 | Max approved fee in bps (0 revokes; capped at 8) |
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
}
}
| Field | Type | Description |
|---|---|---|
signers | array of hex addresses | The multi-sig signer set |
threshold | uint32 | M-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 |
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: []withthreshold: 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
}
}
| Field | Type | Range / values | Description |
|---|---|---|---|
user | hex address | 40 hex chars | The multi-sig account. It must already carry a registered roster |
inner_action_blob | hex string | 0x-hex, non-empty | The 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 |
signatures | array of hex strings | each 65 bytes | Roster signatures over the inner digest. There is no per-entry signer field — the signer is recovered |
nonce | uint64 | The 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 call | Result |
|---|---|
user has no registered roster, or a zero threshold | Rejected — user not multi-sig / multi-sig threshold zero |
An empty inner_action_blob | Rejected, InvalidParams — empty inner_action_blob |
| A signature of the wrong length, or from a non-roster key | Silently 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 recovered | Rejected, AUTH_UNAUTHORIZED |
A stale or replayed params.nonce | Rejected — stale or replayed multi-sig nonce |
An inner action outside the executable set: a governance vote, a system write, or a nested multi_sig | Rejected, 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 user | Rejected, InvalidParams — inner_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.
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
}
}
| Field | Type | Description |
|---|---|---|
name | string | Human-readable label for the sub-account (non-empty) |
explicit_index | uint32 | null | Optional explicit sub-account index; null = use the next free index. An in-use explicit index is rejected at commit (index in use) |
shared_stp_group | bool | Whether 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"
}
}
| Field | Type | Description |
|---|---|---|
sub_index | uint32 | Index of the sender's sub-account (as assigned at create time) |
deposit | bool | true = master → sub; false = sub → master |
amount | decimal string | Cross-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"
}
}
| Field | Type | Description |
|---|---|---|
sub_index | uint32 | Index of the sender's sub-account |
token | uint32 | Spot token id to move |
deposit | bool | true = master → sub; false = sub → master |
amount | decimal string | Token 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 }
}
| Field | Type | Values | Description |
|---|---|---|---|
owner | hex address | omitted | 40 hex chars | Optional: toggle as this account (approved agents only). Not digest-bound — resolved at admission |
hedge | bool | true / false | true = 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).
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
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" }
}
| Field | Type | Description |
|---|---|---|
kind | uint8 | 0 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. |
value | decimal (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.
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:
| Message | Cause |
|---|---|
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 mode | a reservation set on a unified account |
reservations exceed account value | an INCREASE whose new total exceeds account value |
cannot change abstraction while enrolled in portfolio margin | PM enrolled |
cannot change abstraction with <surface> | the account is not flat |
a split standard account has no reservations | kind 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 mode | kind: 0, value: 1 while the perp wallet is below zero |
spot wallet is negative; cannot leave standard mode | kind: 0, value: 0 while the split account's spot wallet is below zero |
Set another user's abstraction config
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"
}
}
| Field | Type | Description |
|---|---|---|
user | hex address | The user whose config the agent is updating |
kind | uint8 | Sub-type tag |
value | decimal (string or number) | Setting value |