Agara

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:submit scope (Settings → API Tokens → Submit account batches), exchange set to AGARA. It is a dedicated scope: granting orders:place_signed does not grant it.
  • You sign an AgaraAccount batch 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:

{ "kind": "SPLIT", "market_id": "0b0e7f3a-…", "condition_id": "0x…", "shares_micro": 5000000 }

Merge burns the full binary pair back to collateral for every AGARA market:

{ "kind": "MERGE", "market_id": "7c2d9e1b-…", "condition_id": "0x…", "shares_micro": 2000000 }

Withdraw moves collateral out of the account:

{ "kind": "WITHDRAW", "destination": "0x5555…", "amount_micro": 1000000 }

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": 4,
  "deadline_unix_seconds": 1784022000,
  "signature": "0x5ee4523b…",
  "ops": [
    { "kind": "SPLIT", "market_id": "0b0e7f3a-…", "condition_id": "0x…a1", "shares_micro": 5000000 },
    { "kind": "MERGE", "market_id": "7c2d9e1b-…", "condition_id": "0x…a2", "shares_micro": 2000000 },
    { "kind": "WITHDRAW", "destination": "0x5555…", "amount_micro": 1000000 }
  ]
}
  • seq: the sequence number this batch takes. Read your account's current one on-chain by calling seq() (selector 0x6857ab40). 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-byte r‖s‖v from 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:

{ "batch_hash": "0x51cda675…" }

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.

HTTPtokenmeaning
409batch_in_flightyour queue is full, a batch of yours already holds that seq, or a merge-all is settling
409seq_mismatchseq is behind the account, or above a seq no batch of yours holds (the message carries both)
409heal_rejecteda recovery batch was refused
409(none)your wallet isn't approved yet; finish onboarding first
422bad_signaturethe recovered signer isn't the account owner (usually your composition differs from the canonical one below)
422insufficient_coveragethe batch's total demand exceeds your available collateral or positions
422value_cap_exceededthe batch value is above the configured cap
422market_resolveda split or merge targeted a resolved market
422market_not_foundthe condition is unknown or doesn't match the market
422unsupported_routean operation has no supported contract route in this environment
422amount_not_representablean amount isn't a whole multiple of the market's lot size
422deadline_invalidthe 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)
422invalid_op / batch_sizean 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

{
  "batch_hash": "0x51cda675…",
  "status": "SETTLED",
  "seq": 4,
  "deadline_unix_seconds": 1784022000,
  "origin": "PRESIGNED",
  "tx_hash": "0x5e0d…",
  "executed_at": "2026-07-14T09:12:41Z",
  "failure_reason": null,
  "superseded_by_batch_hash": null,
  "heals_batch_hash": null,
  "unwound_at": null,
  "created_at": "2026-07-14T09:12:03Z"
}

Poll every 1 to 2 seconds until a final state. The states:

statusfinalmeaning
PENDINGnoaccepted; split, merge, and withdrawal changes become usable at SUBMITTED
SUBMITTEDnothe transaction is on its way to the chain
SETTLEDyesconfirmed on-chain and recorded; tx_hash and executed_at are set
FAILEDyesit 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_DIVERGENTyesthe 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 201 before 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 transactionfailure_reason contains SEQ_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 batch or 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:

EIP712Domain {
  name:              "AgaraAccount"
  version:           "1"
  chainId:           <environment chain id>
  verifyingContract: <your account address>
}

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:

Call(address target, uint256 value, bytes data)
Batch(address wallet, uint256 seq, uint256 deadline, Call[] calls)

wallet is your account address (the same value as verifyingContract). Each op becomes one Call, in op order, with value always 0:

optargetcalldata
SPLITConditionalTokenssplitPosition(collateral, 0x00…00, condition_id, [1,2], shares_micro)
MERGEConditionalTokensmergePositions(collateral, 0x00…00, condition_id, [1,2], shares_micro)
WITHDRAWcollateral tokentransfer(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.

account    = 0x1111111111111111111111111111111111111111
ctf        = 0x2222222222222222222222222222222222222222
collateral = 0x4444444444444444444444444444444444444444
chainId    = 8453
seq        = 4
deadline   = 1784022000
key        = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

ops = [ SPLIT condition 0xaa…a1 (62 a's, then a1), shares_micro 5000000 ]

batch_hash = 0x51cda675bcc49a94f2c4ace692aa542d182dcb43ffe000da5d343bb49a441574
signature  = 0x5ee4523b8cfe07be0dce5a8f42d385b70c9d171f526a71c158f795b76bfd6b22
             6a31706a3babc9fb4d5b4aa7676251c0daae344894c49cb753f71cbed88457da1c

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_at marks the whole merge done.
  • Each listed batch_hash is a regular batch readable at GET /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 destination before you sign: transfers are irreversible once the batch settles, and a withdrawal to your own account is rejected.

On this page