Pre-signed orders
Sign orders locally with the wallet's private key and submit them in a single round-trip. The fast path for high-frequency Agara market making.
Pre-signed orders let a bot sign locally with the account owner's key and submit the completed envelope to a dedicated endpoint. The request still requires PAT authentication, but it does not wait for server-side signing. Measure the path in your deployment rather than depending on an undocumented latency target.
For signed inventory and withdrawal operations, see account batches.
When to use it
Pre-signed orders are worth the setup if:
- You're running a market-making bot or any high-frequency strategy where the per-order signing round-trip is your bottleneck.
- You can hold a private key in your bot's process or KMS.
If you're placing one-off orders from a browser, or your throughput is already comfortably under the regular endpoint's limits, don't bother — server-signed orders are simpler and the latency difference won't matter to you.
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
chain_token_id must encode the same unsigned integer as token_id. A
mismatch returns 400.
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 400.
What the server checks
Each pre-signed order is validated before it reaches the book. Envelope and
request validation failures return 400; live balance, inventory, post-only,
and fillability refusals return 422.
| If… | you get |
|---|---|
type isn't LIMIT | pre-signed endpoint accepts LIMIT orders only |
chain_token_id ≠ token_id as numbers | chain_token_id does not match token_id |
maker isn't your deposit-wallet address | maker must equal the wallet's deposit-wallet address |
order_hash ≠ the server's recompute | order_hash does not match the recomputed EIP-712 digest |
| the signature is high-s | signature s is not canonical (high-s) |
| the signature doesn't recover to the wallet owner | signature does not recover to the wallet's EOA |
| notional is outside the beta band | order notional is below the $0.10 beta minimum / …exceeds the $100,000 beta maximum |
If every order fails with the hash-mismatch message, check the chain ID and standard exchange address first. The signature is bound to both.
The active values are chain ID 8453 and standard exchange
0xD06f3fA925D35077A11dB42c6614D684f10B2aD6.
Response
Identical to the regular endpoint — 202 Accepted with the order ID:
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:
every order event on the account stream carries 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 returns 409 rather than creating
a second order. 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 retrying an uncertain request.
Restrictions
- LIMIT only. A market order's result depends on live orderbook depth, so send market orders to the regular endpoint.
- Time-in-force.
GTC,GTD,FAK, andFOKall work. AGTDorder needsexpiration_unix_secondsat least 30 seconds in the future — 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 with400before 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:
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:
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 code and message — a re-submitted order
(same order_hash) comes back as duplicate_order_hash, an order that
fails validation comes back with the matching reason. 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
400 — 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 — back off and retry the same batch (already
accepted orders dedupe by order_hash, so a retry is safe).
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.
Type — the Order struct has exactly these nine fields, in this
order:
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.