# Account batches

> Sign a split, merge, or withdrawal with your own account key and submit it yourself, one operation per batch.

An account batch is a split, merge, or withdrawal that you sign with your
account's owner key and submit yourself. The
[split and merge endpoints](/docs/on-chain/positions) create the same kind of
batch and sign it for you.

Each batch carries **exactly one operation**. A batch with more operations is
refused with `422 invalid_batch`. To run several operations, sign one batch for
each and queue them; see [Queueing several batches](#queueing-several-batches).

The only exception is a cross-chain withdrawal, which sends its approval and
its withdrawal together in one batch. You cannot sign one yourself: start it
from the signed-in web flow, which builds and signs it from a current quote.

A batch executes as one on-chain transaction.
Provisional balance and position changes can appear while the batch is still
`PENDING`; do not rely on them yet. Use `SUBMITTED` as the conservative
client gate: it means the provisional account update completed and chain
submission is beginning or in progress. Treat those changes as provisional
until `SETTLED`.

<Callout type="info" title="API order placement">
For API trading, use [pre-signed orders](/docs/api-traders/pre-signed-orders).
Server-side signing may slow order ingestion. Follow the
[one-time setup](/docs/api-traders/pre-signed-orders#one-time-setup) and grant
`orders:place_signed` for signed single-order and batch submissions.
</Callout>

## When to use it [#when-to-use-it]

Reach for a batch when you hold your wallet's private key and want to submit a
ready-made signed operation, the same way you already do with
[pre-signed orders](/docs/api-traders/pre-signed-orders).

If you would rather not sign, the split and merge endpoints in
[On-chain positions](/docs/on-chain/positions) are simpler.

## One-time setup [#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](/docs/api-traders/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](#signing-a-batch) below.

## The operations [#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](/docs/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:

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

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

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

**Withdraw** moves collateral out of the account:

```json
{ "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 or the collateral
token's own address. The same-chain operation is available in every
environment.

The operation becomes one on-chain call, and its events carry `batch_index = 0`.

Cross-chain batches use an exact two-operation approval and withdrawal pair
that the server builds and signs from a current quote in the signed-in web
flow. 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. A batch you sign with
these kinds is refused with `422 invalid_batch`. They still share your account
sequence: the approval is call 0 and the withdrawal call 1, so the withdrawal's
events carry `batch_index = 1`.

## Submitting a batch [#submitting-a-batch]

`POST /trade/v1/batches`

**Scope:** `batches:submit`

### Request [#request]

```json
{
  "seq": 4,
  "deadline_unix_seconds": 1784022000,
  "signature": "0x5ee4523b…",
  "ops": [
    { "kind": "SPLIT", "market_id": "0b0e7f3a-…", "condition_id": "0x…a1", "shares_micro": 5000000 }
  ]
}
```

* `ops`: a list holding the batch's one operation.
* `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](#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 cannot execute by then expires without consuming the seq.
  A transaction executed before the deadline can confirm afterward and still
  settle. Allow for execution, confirmation, and queue time: 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](#signing-a-batch)).

### Response [#response]

`201 Created` with the batch's hash, the same digest you signed and can
compute locally before you submit:

```json
{ "batch_hash": "0x51cda675…" }
```

If the response is lost, read the batch by the hash you computed before
submission. A byte-identical replay cannot create a second batch identity.
However, the current wallet, market, deadline, value, and signature checks are
reevaluated and can return a typed refusal instead of `201`. The hash lookup,
not a promised replay status, is authoritative.

### What the server checks [#what-the-server-checks]

Failures use [Problem Details](/docs/errors#http-problem-details). Match the
stable `code`, never the first word of `detail`.

| Status and code                       | Meaning and action                                                                                                                                                                           |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `409 batch_in_flight`                 | A batch already owns this sequence or the queue is full; wait and read current state                                                                                                         |
| `409 sequence_conflict`               | The sequence is no longer current; re-read `seq()` before signing again                                                                                                                      |
| `403 forbidden_scope`                 | The PAT lacks `batches:submit`                                                                                                                                                               |
| `403 account_quarantined`             | Stop new batches until the account is available again                                                                                                                                        |
| `404 wallet_not_registered`           | Register the AGARA wallet                                                                                                                                                                    |
| `422 invalid_signature`               | Rebuild the canonical envelope and sign with the account owner                                                                                                                               |
| `422 insufficient_balance`            | The batch demand exceeds available coverage; reconcile and reduce it                                                                                                                         |
| `422 value_cap_exceeded`              | Reduce the batch value                                                                                                                                                                       |
| `422 market_closed`                   | The market is closed or resolved; split and merge no longer run there                                                                                                                        |
| `422 invalid_batch`                   | Send one operation per batch, and correct the operation, deadline, route, or amount                                                                                                          |
| `424 wallet_setup_incomplete`         | Finish wallet setup before submitting                                                                                                                                                        |
| `502 dependency_outcome_unknown`      | Follow `recovery.resource.batch_hash`; check that batch before another mutation                                                                                                              |
| `502 dependency_credentials_rejected` | Stop retrying, retain `request_id`, and contact support                                                                                                                                      |
| `502 dependency_invalid_response`     | Do not retry automatically; retain `request_id` for investigation                                                                                                                            |
| `503 dependency_unavailable`          | Back off with jitter, then reconcile before resubmitting                                                                                                                                     |
| `503 account_operations_paused`       | Retry in a few minutes: new batches are paused for maintenance. A heal is still accepted, and batches you already submitted still settle, so read their state rather than signing them again |
| `503 feature_not_configured`          | This deployment does not offer account batches                                                                                                                                               |

Coverage is checked against your balance and positions **before** the batch.
When one operation needs what another produces, sign the second batch after
the first settles.

## Tracking a batch [#tracking-a-batch]

`GET /trade/v1/batches/{batch_hash}`

**Scope:** `batches:submit`

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

For a terminal failure, `failure` is a safe typed object:

```json
{
  "status": "FAILED",
  "failure": {
    "code": "sequence_conflict",
    "title": "Sequence conflict",
    "detail": "Order submission failed, please retry.",
    "recovery": { "strategy": "none" }
  },
  "unwound_at": "2026-07-14T09:13:10Z"
}
```

Branch on `failure.code`; do not parse `title` or `detail`.

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

| status             | final       | meaning                                                                                                                                                                                                   |
| ------------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PENDING`          | no          | accepted; provisional effects may already appear, but clients should not rely on them yet                                                                                                                 |
| `SUBMITTED`        | no          | the provisional account update completed and chain submission is beginning or in progress; effects remain reversible until settlement                                                                     |
| `SETTLED`          | yes         | confirmed on-chain and recorded; `tx_hash` and `executed_at` are set                                                                                                                                      |
| `FAILED`           | conditional | no successful execution is expected, but possibly applied effects can still be reversing; require `unwound_at`, and apply the stranded-higher-sequence exception below before trusting restored inventory |
| `FAILED_DIVERGENT` | yes         | the failure needs review; the account stays frozen until it is resolved                                                                                                                                   |

Read `FAILED` together with `unwound_at`. For an isolated failed batch,
`unwound_at` normally means its balance and position effects were reversed.
When it is still `null`, treat the funds as not yet restored and keep polling.
A reversal that cannot complete can instead move the batch to
`FAILED_DIVERGENT`. There is one safety exception: when a lower sequence is
`FAILED_DIVERGENT`, a higher queued batch can be closed as `FAILED` with
`sequence_conflict` while its effects are still uncertain. For those higher
rows, `unwound_at` alone is not permission to reuse inventory. Keep account
mutations paused, reconcile every affected hash, the on-chain sequence,
balances, positions, and account events, and resolve the divergent batch first.

If you already listen to the
[account stream](/docs/account-stream#batch-provenance), 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 [#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 `sequence_conflict`, 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](#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.

Automatic redemption does not use your account sequence. It starts only after
wallets with a position in the market have no `PENDING` or `SUBMITTED` account
batches, so a queue you keep continuously non-empty can delay that market's
redemption.

The platform-signed transfer that moves bridged funds does use your account
sequence, but it is not inserted behind batches you already queued. It waits
until that queue finishes. Keeping the queue continuously non-empty can
therefore delay bridged funds indefinitely.

### What you observe on failure [#what-you-observe-on-failure]

For an isolated `FAILED` batch, `unwound_at` normally means its split, merge,
or withdrawal effects were reversed. Do not apply that shortcut to higher
`sequence_conflict` rows stranded by a lower `FAILED_DIVERGENT` batch; keep the
account paused and use the full reconciliation described above.

Whenever recovery requires positions, retry the whole positions read while
`unavailable_exchanges` includes `AGARA`. Do not rebuild the queue from a
partial `200` response.

What happens to the rest of your queue depends on `failure.code`:

* **`sequence_conflict`** — the sequence is no longer safe to attribute to this
  batch. Another transaction may have consumed it, a lower failed sequence may
  have stranded this batch, or recovery may have raced. Do not infer that later
  queued sequences can run from this code alone. Poll every affected hash until
  terminal and re-read `seq()`, balances, positions, and account events. If a
  lower batch is `FAILED_DIVERGENT`, do not trust a higher row's `unwound_at` or
  rebuild the queue until that divergence is resolved.
* **`batch_superseded`** — a replacement or recovery batch took over. Follow
  `superseded_by_batch_hash` and reconcile that batch.
* **`chain_execution_failed`, `insufficient_balance`,
  `insufficient_shares`, or `internal_error`** — do not assume a later queued
  batch can run. Poll every affected hash until terminal, re-read `seq()`, and
  rebuild from authoritative balances and positions.
* **`settlement_divergent`** — stop submitting. The account needs review before
  more mutations are safe.

When a non-divergent failure strands higher sequences, poll those hashes until
each is terminal, then re-read `seq()` and reconcile balances, positions, and
account events before signing again. If the lower batch is
`FAILED_DIVERGENT`, stay paused instead. 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 while the stranded original is still
`PENDING`. Before it becomes terminal, call
`POST /trade/v1/batches/{batch_hash}/supersede` with a replacement signed at
the same sequence. Once the original reads `FAILED`, the supersede endpoint
only returns that terminal row; re-read `seq()`, balances, and positions, then
submit a fresh batch if the reconciled state permits it.

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 [#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
`422 invalid_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`:

| 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`](https://basescan.org/address/0xe12D566cE5Dd8d817488E85a81386878649e3A18) and the
collateral address is [`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`](https://basescan.org/address/0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913). Verify them against
[CTF tokens](/docs/tokens/ctf) and [Collateral](/docs/tokens/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 [#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.

## Security considerations [#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](/docs/api-traders/pre-signed-orders#security-considerations).
* 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.
