Account batches
Sign one envelope that runs several split, merge, or withdrawal operations as a single on-chain transaction. The atomic inventory primitive for self-custody makers.
Split and merge normally run as separate signed transactions. When several independent operations need to succeed or fail together, sign one account batch. For example, merge complete pairs from two markets in one transaction.
A batch is one signature over an ordered list of 1 to 20 operations. It
executes as a single on-chain transaction: either every operation lands or
none do. Balance and position changes become available when the batch reaches
SUBMITTED, not merely when the request is accepted as PENDING. Treat those
changes as provisional until SETTLED.
When to use it
Reach for a batch when:
- You want independent inventory operations to settle atomically.
- You're a self-custody maker holding the wallet's key and want to submit a ready-made signed envelope, the same way you already do with pre-signed orders.
If you only ever run a single split or merge at a time, the per-position endpoints in On-chain positions are simpler. Reach for batches when atomicity across operations is the point.
One-time setup
Batches are signed against your account (the deposit wallet), with the same owner key and export flow as pre-signed orders. Follow Pre-signed orders → One-time setup to export the key and note the account address. Two differences:
- Mint the PAT with the
batches:submitscope (Settings → API Tokens → Submit account batches), exchange set to AGARA. It is a dedicated scope: grantingorders:place_signeddoes not grant it. - You sign an
AgaraAccountbatch envelope, not an exchange order. The schema is in Signing a batch below.
The operations
Every quantity is an integer in micro units (one collateral unit = 1,000,000; outcome
shares use the same scale). condition_id is the market's 32-byte id
from Markets.
Split locks collateral and mints the full binary pair. AGARA neg-risk group members are ordinary binary CTF conditions and use this same operation:
Merge burns the full binary pair back to collateral for every AGARA market:
Withdraw moves collateral out of the account:
Withdrawals floor at 10,000 micro (0.01 collateral units), count toward the per-batch value cap, and must not target your own account. The same-chain operation is available in every environment.
Cross-chain batches use an exact two-operation approval and withdrawal pair
from a current server quote. The wire kinds are ACROSS_SWAP_APPROVE followed
by WITHDRAW_ACROSS_SWAP_DEPOSIT, WITHDRAW_ACROSS_SWAP_DEPOSIT_V3, or
WITHDRAW_ACROSS_SWAP_CCTP, with equal input amounts. Treat the returned kind
as opaque and do not construct this pair from stale quote data.
Operation order is significant: operation i is call i on-chain, and its
events use batch_index = i. Coverage is calculated from balances held before
execution, so an earlier operation cannot fund a later one.
Submitting a batch
POST /trade/v1/batches
Scope: batches:submit
Request
seq: the sequence number this batch takes. Read your account's current one on-chain by callingseq()(selector0x6857ab40). A batch executes only when the account has reached its seq, and executing it consumes the seq, so a signed batch runs at most once. To queue several batches, give each the next seq up from the last — see Queueing several batches.deadline_unix_seconds: when the signature expires, enforced on-chain. It must be in the future and at most just under 24 hours ahead (a one-minute clock-skew margin is reserved at the top of the window). Keep it short, 10 to 30 minutes: the deadline is your safety valve, and a batch that can't confirm by then fails cleanly and frees the seq. Allow for queue time if you are queueing: the deadline runs from when you sign, not from when the batch reaches the front.signature: the 65-byter‖s‖vfrom the account owner key over the batch digest (see Signing a batch).
Response
201 Created with the batch's hash, the same digest you signed and can
compute locally before you submit:
Re-submitting a byte-identical accepted batch returns 201 with the same
hash, so a timed-out POST is safe to retry. You can also read the batch by the
hash you computed before submission.
What the server checks
The 409 and 422 refusals below carry a stable machine token as the
first word of the message; match on that. Operation-specific failures
include the operation index.
| HTTP | token | meaning |
|---|---|---|
409 | batch_in_flight | your queue is full, a batch of yours already holds that seq, or a merge-all is settling |
409 | seq_mismatch | seq is behind the account, or above a seq no batch of yours holds (the message carries both) |
409 | heal_rejected | a recovery batch was refused |
409 | (none) | your wallet isn't approved yet; finish onboarding first |
422 | bad_signature | the recovered signer isn't the account owner (usually your composition differs from the canonical one below) |
422 | insufficient_coverage | the batch's total demand exceeds your available collateral or positions |
422 | value_cap_exceeded | the batch value is above the configured cap |
422 | market_resolved | a split or merge targeted a resolved market |
422 | market_not_found | the condition is unknown or doesn't match the market |
422 | unsupported_route | an operation has no supported contract route in this environment |
422 | amount_not_representable | an amount isn't a whole multiple of the market's lot size |
422 | deadline_invalid | the deadline is past or beyond the ceiling of just under 24 hours (a one-minute clock-skew margin is reserved at the top of the window) |
422 | invalid_op / batch_size | an operation is malformed, or the batch has 0 or more than 20 |
403 | (none) | the PAT lacks batches:submit |
403 | (none) | your wallet is frozen; only recovery submissions are accepted |
404 | (none) | batches aren't enabled on this deployment |
502 | (none) | the service is temporarily unavailable; retry later |
Coverage is checked as the sum of every operation's inputs against your balance and positions before the batch. Operations can't consume what earlier operations in the same batch produce: split a dependent flow across two batches, the second signed after the first settles.
Tracking a batch
GET /trade/v1/batches/{batch_hash}
Scope: batches:submit
Poll every 1 to 2 seconds until a final state. The states:
| status | final | meaning |
|---|---|---|
PENDING | no | accepted; split, merge, and withdrawal changes become usable at SUBMITTED |
SUBMITTED | no | the transaction is on its way to the chain |
SETTLED | yes | confirmed on-chain and recorded; tx_hash and executed_at are set |
FAILED | yes | it did not and never will execute (deadline expired, or the seq was taken by another transaction); its effects are being reversed, and unwound_at tells you when that finished, after which the seq is free to re-read seq() and sign a new batch |
FAILED_DIVERGENT | yes | the failure needs review; the account stays frozen until it is resolved |
Read FAILED together with unwound_at. When unwound_at is set, every
balance and position effect has been fully reversed. When it is still
null, the reversal is completing: treat the batch's funds as not yet
restored and keep polling until unwound_at fills in. A batch closed by
a settled recovery shows unwound_at set at the moment it closed.
If you already listen to the
account stream, use its position and
collateral events to update your live account view. tokens_minted,
tokens_merged, collateral_withdrawn, and compensating
collateral_deposited events carry the (batch_hash, batch_index) pair when
available. The pair correlates an event with an operation; it is not a
deduplication key, and recovery can produce several events with the same pair.
Keep polling GET even after the stream shows the account change. In particular,
collateral_withdrawn means your available collateral was debited; only the
batch's final SETTLED status confirms the on-chain withdrawal. Portfolio REST
and the batch response remain authoritative after reconnects or missed events.
Queueing several batches
You can hold several signed batches at once. Give each the next seq up from the last, and the server runs them strictly in that order: a batch waits until your account reaches its seq, then executes.
Three rules make a queue work:
- Send them one at a time. Wait for each
201before signing the next, so you know which seq it took. - No gaps. Every seq between your account's current one and the batch you
are signing must already be held by a batch of yours. A seq above a gap is
refused with
seq_mismatch, because nothing would ever consume the gap. - There is a limit. Past it you get
batch_in_flight. It is set per deployment; treat a refusal as the signal rather than assuming a number.
Each batch is still independent. Coverage is what you hold now, minus what your
still-waiting batches will spend — so you cannot queue past your balance. Once a
batch starts executing it reads SUBMITTED, and from then on its proceeds count
toward coverage even though they are still provisional. Queueing on top of them
is accepted and is the risk described under
what you observe on failure. For a flow that
truly depends on an earlier result, wait for SETTLED.
A queued batch stays PENDING, and its deadline is running the whole time it
waits. Give a batch you expect to sit behind others a longer deadline, or send
it later. A deadline that lapses in the queue is only noticed when the batch
reaches the front, so it can read PENDING well past its own deadline.
Split, merge, and withdrawal operations you start from the app share the same sequence, so they queue alongside anything you signed yourself.
The platform also signs on this sequence — to redeem your resolved markets and to move bridged funds — but it cannot queue behind you: those operations wait until your queue drains. A queue you keep permanently full delays them indefinitely.
What you observe on failure
A FAILED batch with unwound_at set has been fully reversed: any
split's shares removed and collateral returned, any merge's shares
restored, any withdrawal re-credited.
What happens to the rest of your queue depends on why it failed, and
failure_reason tells you which:
- Its seq was taken by another transaction —
failure_reasoncontainsSEQ_CONSUMED. The account moved on without this batch, so the rest of your queue still runs, just without its operations. - Anything else — a revert, an expired deadline, or a refused operation.
Nothing will ever take that seq, so every batch you signed above it fails too,
with
stranded by divergent batchor a note that a lower seq it depended on will not be consumed.
For the second case, poll the hashes above it until each reads FAILED, then
re-read seq() and sign again. Allow at least a minute, usually one to two: a
transaction still in flight looks exactly like an abandoned seq at first, so the
server waits to be sure before failing anything.
You can also keep the queue alive instead. Sign a replacement at the failed batch's own seq within that window and the batches above it resume rather than failing.
A batch that needs review (FAILED_DIVERGENT) is the exception to the waiting
period: it closes the rest of your queue immediately.
Freshly split or merged inventory is spendable during the in-flight window,
but using it can leave the automatic reversal short if the batch fails. Do not
commit a batch's proceeds to other fills or withdrawals until it reads
SETTLED.
Signing a batch
The signature is EIP-712 over the account's Batch struct. Get any
field wrong and the server's recompute won't match, so you get
bad_signature.
Domain: bind to your own account, or every batch hash-mismatches:
verifyingContract is your account address, not an exchange or registry.
Use chain ID 8453 for this deployment.
Types: mirror the Agara account contract's Call and Batch structs exactly:
wallet is your account address (the same value as
verifyingContract). Each op becomes one Call, in op order, with
value always 0:
| op | target | calldata |
|---|---|---|
| SPLIT | ConditionalTokens | splitPosition(collateral, 0x00…00, condition_id, [1,2], shares_micro) |
| MERGE | ConditionalTokens | mergePositions(collateral, 0x00…00, condition_id, [1,2], shares_micro) |
| WITHDRAW | collateral token | transfer(destination, amount_micro) |
The Conditional Tokens address is 0xe12D566cE5Dd8d817488E85a81386878649e3A18 and the
collateral address is 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913. Verify them against
CTF tokens and Collateral.
batch_hash is the EIP-712 signing digest; sign it with the account
owner key (canonical low-s, EIP-2), no extra prefix.
Verify against this vector
Reproduce this before pointing a hand-rolled signer at a live environment. All inputs are illustrative; the signing key is the well-known Anvil test key 0, never for use outside a test.
If your digest doesn't match, your composition differs from the canonical one. The server only ever accepts the exact bytes this composition produces, so fix the digest before anything else.
Batch groups
Merge-all settles through the same batch machinery you use directly: it creates a batch group — one or more platform-signed batches, each covering up to 20 markets. You will see them when polling:
GET /trade/v1/batch-groups/{group_id}lists the group's batches in order with their statuses;completed_atmarks the whole merge done.- Each listed
batch_hashis a regular batch readable atGET /trade/v1/batches/{batch_hash}.
Group batches are managed for you: they cannot be superseded or healed
through the endpoints above, and while a group is still settling, new
batch submissions on the same wallet are refused with batch_in_flight —
wait for the group's completed_at, then retry.
Security considerations
- The account owner key authorises batches that move funds and positions directly. Anyone who reads it can drain the account. Store it as a high-impact signing credential, exactly as for pre-signed orders.
- The PAT alone can't forge a batch: every submission also needs a valid owner signature. A leaked PAT is far less dangerous here than on a server-signed path; revoke it in Settings → API Tokens to cut off submissions without touching the key.
- Verify a withdrawal
destinationbefore you sign: transfers are irreversible once the batch settles, and a withdrawal to your own account is rejected.