Pre-signed orders
The recommended order-placement flow for API trading. Sign locally and submit single orders or batches.
API order placement
For API trading, use pre-signed orders. Server-side signing may slow order ingestion.
Sign locally with the account owner's key and submit the completed envelope.
The request still requires PAT authentication with orders:place_signed.
Complete the one-time setup, then use the
single-order endpoint or
batch endpoint.
For signed inventory and withdrawal operations, see account batches.
When to use it
Use this flow for API trading at any order frequency. Your trading client needs access to the owner's signing key, held securely in your process or signing service. Browser trading can continue to use server-signed orders.
Pre-signed placement supports LIMIT orders only. For immediate execution,
use a LIMIT order with your worst acceptable price and FAK or FOK time in
force. FAK permits partial fills and cancels the remainder; FOK requires a
complete fill. The existing server-signed endpoint also supports MARKET
orders, but a MARKET request cannot be sent to a signed-order endpoint.
One-time setup
1. Export the wallet's private key
- Open Settings in the web app.
- Find the Wallet details card. The bottom row is labeled Export private key with a Reveal button.
- Click Reveal. A secure modal opens — this is run by Privy on a separate origin, so neither the web app nor anything else on the page can read the key as it's displayed.
- Copy the 0x-prefixed hex string out of the modal and close it. The modal can be reopened later if you lose the copy.
Store the key somewhere your bot process can read at startup — KMS, an encrypted secret manager, an environment variable on a hardened host. Treat it as a high-impact signing credential: anyone who reads it can sign on behalf of the wallet, place signed orders, and authorize on-chain account batches.
The key is the EOA owner of the deposit wallet, not the deposit wallet itself. Rotating it means moving funds to a fresh wallet through normal onboarding; there's no in-place rotation today.
2. Mint a PAT with the orders:place_signed scope
In Settings → API Tokens, create a new token and check the Place pre-signed orders scope. Make sure the token's exchange is set to AGARA.
You can grant this scope alongside the usual ones
(orders:cancel, portfolio:read, account:stream) — same token
covers the whole bot loop.
3. Note the deposit-wallet address and chain settings
The bot signs against the deposit wallet, not the EOA directly. Grab the AGARA deposit wallet address from the same Wallet details card — the row labeled "AGARA deposit wallet — holds funds" has a copy button next to it.
The chain ID and standard exchange address are deployment-specific signing
values. This deployment uses chain ID 8453 and standard exchange
0xD06f3fA925D35077A11dB42c6614D684f10B2aD6. Confirm both against CTF tokens.
The signed-order validator uses this standard exchange as the verifying contract. Do not substitute the neg-risk exchange address.
Sending an order
POST /trade/v1/orders/signed
Scope: orders:place_signed
Request
{
"token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455",
"side": "BUY",
"type": "LIMIT",
"time_in_force": "GTC",
"price_micro": "600000",
"shares_micro": "1000000",
"post_only": false,
"salt": "84629103847263918473",
"maker": "0xCdEf012345...",
"chain_token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455",
"maker_amount": "600000",
"taker_amount": "1000000",
"side_u8": 0,
"timestamp": "0",
"metadata": "0x0000000000000000000000000000000000000000000000000000000000000000",
"builder": "0x0000000000000000000000000000000000000000000000000000000000000000",
"order_hash": "0xfe738cc74603ab47...",
"signature": "0x4f3a8e9b..."
}chain_token_id must encode the same unsigned integer as token_id. A
mismatch returns 422 validation_failed.
The first block of fields (token_id through post_only) is identical
to the regular Place an order body. The
second block is the EIP-712 envelope your bot signed over.
side_u8, timestamp, metadata, and builder are canonical
envelope values produced by sign_limit_order and should not be
hand-constructed. timestamp and metadata are zero placeholders
today; builder is unused. If you're implementing without the SDK,
mirror these values exactly.
The server recomputes the order hash from the envelope fields, asserts
it matches the order_hash you sent, recovers the signing address from
signature, and confirms it matches the wallet's owner. Any
mismatch returns 422 validation_failed.
What the server checks
Each pre-signed order is validated before it reaches the book. Well-formed
request and envelope failures return 422 with a stable code. Malformed JSON
is the distinct 400 malformed_json transport error.
| If… | code |
|---|---|
| The request shape, envelope, signature, grid, or hash is invalid | validation_failed |
| The order value is under the 0.10 collateral-unit minimum | order_value_below_minimum |
| The order value is over the 100,000 collateral-unit maximum | order_value_above_maximum |
| The limit price is not above 0 and below 1,000,000 micro-units | order_price_out_of_range |
| A GTD expiration is under 30 seconds away | order_expiration_too_soon |
| The request reaches the identity check and the signed order hash already exists | duplicate_order |
| The market is not accepting orders | market_closed |
| Collateral or shares are insufficient | insufficient_balance or insufficient_shares |
| A post-only order would take | post_only_would_cross |
| An immediate order cannot satisfy its instructions | order_not_fillable |
The response intentionally does not reveal which signature check failed. Run
client-side preflight vectors for the chain ID, exchange address, maker, hash,
low-s signature, and recovered owner. If every order fails validation, check
the chain ID and standard exchange address first. Log request_id for
diagnosis; do not parse detail.
The active values are chain ID 8453 and standard exchange
0xD06f3fA925D35077A11dB42c6614D684f10B2aD6.
Response
Identical to the regular endpoint — 202 Accepted with the order ID:
{
"order_id": "9c4a3e7d-12b4-4f8e-9a3c-d2c7f0a45e1b",
"source": "AGARA",
"status": "PENDING",
"pending_operation": "SUBMIT",
"as_of": "2026-05-21T17:20:01.633Z"
}Track the order through account_events the same
way you would for server-signed orders.
Because you know each order's order_hash before you submit it, you
don't have to rely on this response to correlate what happens next:
accepted, cancelled, and fill events on the
account stream carry the order_hash, and you can
fetch an order by it at any time with
GET /trade/v1/orders/by-hash/{order_hash}.
This is the reliable way to tie acks and fills back to the right
submission when you fire many orders at once, and your recovery path if
you ever miss this response.
Submitting the same signed envelope again cannot create a second order, but
current preflight checks can return a typed failure before duplicate detection.
Look up its original state with GET /trade/v1/orders/by-hash/{order_hash}.
Use a fresh salt for each distinct order, but preserve the original signed
envelope when reconciling an uncertain request.
Restrictions
- LIMIT only. The signed-order endpoints do not accept
MARKETrequests. For immediate execution, use a LIMIT order with a price bound andFAKorFOK, as described in When to use it. - Time-in-force.
GTC,GTD,FAK, andFOKall work. AGTDorder needsexpiration_unix_secondsat least 30 seconds in the future, or it is rejected with422 order_expiration_too_soon— same rules as the regular endpoint's time in force. - Order size. During beta, an order's notional
(
price_micro × shares_micro / 1_000_000) must be at least 0.10 collateral units and at most 100,000 collateral units. Outside that band the order is rejected with422 order_value_below_minimumor422 order_value_above_maximumbefore it reaches the book. - Price and size grid.
price_microandshares_micromust fit the market's tick and lot size, or the order is rejected during placement. If you build orders by hand, round to the market's grid from Markets first.
Sending many orders at once
When you re-quote a whole ladder each tick, send the orders together instead of one request per order.
POST /trade/v1/orders/signed/batch
Scope: orders:place_signed
Submit up to 32 pre-signed orders in one call. The request is a
single orders array, where each element is exactly the body you'd send
to POST /trade/v1/orders/signed:
{
"orders": [
{ "token_id": "2174263314...", "side": "BUY", "type": "LIMIT", "...": "..." },
{ "token_id": "2174263314...", "side": "SELL", "type": "LIMIT", "...": "..." }
]
}Response
Every order is checked on its own, so one bad order never sinks the rest. You get back one result per order, in the order you sent them:
{
"results": [
{
"index": 0,
"outcome": "accepted",
"order_id": "9c4a3e7d-12b4-4f8e-9a3c-d2c7f0a45e1b",
"source": "AGARA",
"status": "PENDING",
"pending_operation": "SUBMIT",
"as_of": "2026-05-21T17:20:01.633Z"
},
{
"index": 1,
"outcome": "rejected",
"failure": {
"code": "duplicate_order",
"title": "Duplicate order",
"detail": "An order with this identity already exists.",
"recovery": { "strategy": "none" }
}
}
],
"as_of": "2026-05-21T17:20:01.633Z"
}An accepted result carries the same fields as a single-order response;
track it through account_events the same way. A
rejected result carries a typed failure. A re-submitted hash can return
duplicate_order when it reaches the identity check, but current market,
wallet, signature, and admission checks run first and can return another typed
failure. Look up the hash to decide whether the order exists. Walk the
results array and act on each outcome; don't assume all-or-nothing.
Sending an empty array or more than 32 orders is rejected outright with
422 validation_failed — the whole call, before any order is processed.
Batch calls are metered under their own budget, separate from
single-order placement, so a ladder re-quote can't exhaust the rate
budget your one-off placements draw from. If you exceed it you'll get a
429 with a Retry-After. A received 429 means the batch was not processed;
wait for that interval before retrying it.
A lost response is not a 429. After a timeout or disconnect, look up every
submitted order_hash and retry only envelopes that are still absent. Treat a
duplicate_order result as an existing order to reconcile, not as a newly
rejected quote.
Implementing without the SDK
If you sign in a language the SDK doesn't cover, reproduce exactly what
sign_limit_order does. The order_hash/signature you submit are an
EIP-712 signature over the exchange's Order struct — get any field
wrong and the server's recompute won't match.
Domain — bind the signature to the exact deployment and exchange.
{
"name": "Agara CTF Exchange",
"version": "1",
"chainId": 8453,
"verifyingContract": "0xD06f3fA925D35077A11dB42c6614D684f10B2aD6"
}Type — the Order struct has exactly these nine fields, in this
order:
Order(
uint256 salt,
address maker,
uint256 tokenId,
uint256 makerAmount,
uint256 takerAmount,
uint8 side,
uint256 timestamp,
bytes32 metadata,
bytes32 builder
)Field values:
salt— a fresh randomuint256per order (it's the only thing that makes otherwise-identical orders hash differently).maker— your deposit-wallet address; it's both the source of funds and the account the signature is validated against.tokenId— the outcome's numeric token id (same value aschain_token_id).side—0for BUY,1for SELL.makerAmount/takerAmount— what you give / receive, wherecollateral = shares_micro × price_micro / 1_000_000:- BUY:
makerAmount = collateral,takerAmount = shares_micro - SELL:
makerAmount = shares_micro,takerAmount = collateral
- BUY:
timestamp,metadata,builder— advisory: they're signed over but have no on-chain effect. Always send0/ 32 zero bytes / 32 zero bytes. (They're part of the struct, so you can't omit them — the hash is computed over all nine fields.)
Signature — sign the EIP-712 digest with the deposit wallet's
owner key. It must be a 65-byte r‖s‖v ECDSA signature in canonical
low-s form (EIP-2); the deposit wallet validates it via ERC-1271 by
recovering the owner. Put the digest in order_hash and the signature
in signature, alongside the salt/maker/amounts you signed.
Cancelling and other operations
Cancel, cancel-all, list, and the account stream all work the same way with the same scopes. Only placement has a different endpoint.
Security considerations
- The private key authorises orders and account batches. Anyone who reads it can authorize trades or move the wallet's assets. Store it as a high-impact signing credential.
- The PAT alone is not enough to forge orders — every request must also carry a valid signature from the wallet's owner. A leaked PAT is far less dangerous on this path than on the server-signed path.
- Revoking the PAT in Settings → API Tokens cuts off submissions without affecting the key itself.
- Rotating the key is out of scope here — it means moving funds to a new wallet through the normal onboarding flow.