Agara

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

  1. Open Settings in the web app.
  2. Find the Wallet details card. The bottom row is labeled Export private key with a Reveal button.
  3. 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.
  4. 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 invalidvalidation_failed
The order value is under the 0.10 collateral-unit minimumorder_value_below_minimum
The order value is over the 100,000 collateral-unit maximumorder_value_above_maximum
The limit price is not above 0 and below 1,000,000 micro-unitsorder_price_out_of_range
A GTD expiration is under 30 seconds awayorder_expiration_too_soon
The request reaches the identity check and the signed order hash already existsduplicate_order
The market is not accepting ordersmarket_closed
Collateral or shares are insufficientinsufficient_balance or insufficient_shares
A post-only order would takepost_only_would_cross
An immediate order cannot satisfy its instructionsorder_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 MARKET requests. For immediate execution, use a LIMIT order with a price bound and FAK or FOK, as described in When to use it.
  • Time-in-force. GTC, GTD, FAK, and FOK all work. A GTD order needs expiration_unix_seconds at least 30 seconds in the future, or it is rejected with 422 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 with 422 order_value_below_minimum or 422 order_value_above_maximum before it reaches the book.
  • Price and size grid. price_micro and shares_micro must 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 random uint256 per 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 as chain_token_id).
  • side — 0 for BUY, 1 for SELL.
  • makerAmount / takerAmount — what you give / receive, where collateral = shares_micro × price_micro / 1_000_000:
    • BUY: makerAmount = collateral, takerAmount = shares_micro
    • SELL: makerAmount = shares_micro, takerAmount = collateral
  • timestamp, metadata, builder — advisory: they're signed over but have no on-chain effect. Always send 0 / 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.