TPTokenPlay CLOB APIv1

TokenPlay CLOB Market Maker API

Reference and interactive tester for market makers. OpenAPI JSON: openapi.json. Amounts are decimal strings when they may exceed JavaScript's safe integer range.

Start Here

There are two public surfaces. The matching engine holds resting limit orders. The settlement bridge handles market orders, chain preparation and Solana transaction builders.

NeedUseImportant detail
Read book, place/cancel limit ordersCLOB_API_BASE / /clob/v1API and UI use the same book when the market label is the same.
Market orders and funding helpersCLOB_BRIDGE_BASE / /clob-engineMarket orders fill against the AMM and never rest in the engine book.
EVM integrationexamples/market-maker/mm-client.mjsOrders use EIP-712. Market label is evm:<chainId>:<clobMarketId>.
Solana integrationexamples/market-maker/solana-mm-client.mjsOrders use base58 ed25519 over a canonical string. Market label is the plain numeric CLOB market id.
Deposit, withdraw, claim, LP actionsWallet-signed chain txSolana uses bridge /tx/*. EVM calls ClobExchange directly or via mm-client.mjs.

First-time test order: choose a preset below, click GET /markets, inspect /book, generate a fresh signature, then POST /orders. A copied old signature will be rejected.

Live Market

NetworkBSC Mainnet
App market id68c1a4f0e2b7d93a5f6e8b21
Engine market labelevm:56:1042

The selected network updates the Market field, the wallet, request URLs and both signature scripts. Switching to Solana swaps the signing scheme entirely — see Signature Guide.

Market Maker API List

MethodEndpointUseAuth / Signature
GET/marketsList live CLOB books known to this engine.None
GET/markets/{market}Summary: best bid/ask levels, open orders, session volume.None
GET/markets/{market}/tickerCurrent top-of-book price/spread/session stats.None
GET/markets/{market}/bookAggregated order book depth.None
GET/markets/{market}/ordersAll current resting limit orders.None
GET/users/{user}/orders?market={market}One wallet's current resting orders.None
GET/users/{user}/orders/closed?market={market}One wallet's cancelled, expired, or rejected orders.None
GET/markets/{market}/tradesRecent matched fills observed by this engine.None
GET/orders/{orderId}Status for one order id.None
POST/ordersPlace a limit/market-maker order.EVM: EIP-712 signature. Solana: ed25519 over the canonical string. No API key either way
DELETE/orders/{orderId}Cancel a resting order.Wallet-signed cancel message

Every endpoint above is on this engine and takes no API key — the wallet signature is the authentication. Market orders are not here: they fill against the AMM through the settlement bridge, see Market Orders. Money movement is chain-specific: Solana uses bridge /tx/* transaction builders, while EVM users send ClobExchange contract transactions from their own wallet/RPC or with mm-client.mjs. See Money & Collateral.

Do not give market makers privileged close/settlement/resolve endpoints. Those are operator-only.

Recent Requests

TimeStatusRequest
Make a request to see history.

Suggested Test Flow

StepActionExpected Result
1Click GET /markets or choose a preset.You have the correct engine market label. Do not use the app/Mongo market id in /orders.
2Click book, orders, ticker.You see current book depth, resting orders, and current top-of-book stats.
3Fund the wallet on chain, then place a signed limit order.The order is accepted only if the signature matches the exact payload.
4Click GET /orders/{orderId}.You see the exact order status: open, partial, filled, cancelled, or expired.
5Cancel from UI, or use signed DELETE /orders/{orderId}.The order disappears from book/orders. No explorer tx is expected for engine cancel.
6Check /users/{user}/orders/closed.Cancelled, expired, or rejected orders are visible separately from the live book.
7Withdraw idle balance from the exchange.The wallet receives tokens and this withdrawal appears on the explorer.
8Use a second wallet to cross the book with a taker order.Book size changes and app trade history records the fill. If settlement fails, the engine rolls the fill back.

Market Data Endpoints

GET

Live Markets

Lists live CLOB books known to this engine process. Use this first when you need the exact market label for API calls.

marketEngine label used by /orders, /book, /ticker, and /trades.
countHow many books are currently live in this engine instance.
200Live market summaries returned.
Request
GET/markets
Click Try It
GET

Market Summary

Returns top-level book state for one CLOB market: best prices, open order count, depth levels, and current engine-session volume.

marketEngine label, for example evm:97:314.
200Summary returned.
429Rate limit exceeded.
Request
GET/markets/{market}
Click Try It
GET

Ticker

Returns current best bid, best ask, spread, last trade and session stats for the selected market.

bestBidHighest bid in ticks, if present.
bestAskLowest ask in ticks, if present.
spreadbestAsk - bestBid in ticks, if both sides exist.
Request
GET/markets/{market}/ticker
Click Try It
GET

Price Depth

Returns aggregated bid and ask levels. This is the same shape the order-book widget reads.

bids[]Price levels where users want to buy.
asks[]Price levels where users want to sell.
sizeRaw base units as a decimal string.
Request
GET/markets/{market}/book
Click Try It
GET

Current Orders

Returns current resting/open limit orders. Use the optional user filter to inspect one maker wallet.

userOptional wallet query param.
signatureNever returned from this public endpoint.
Request
GET/markets/{market}/orders
Click Try It
GET

Trades

Returns recent matched fills observed by this engine process.

limitOptional, default 50, max 500.
createdAtISO timestamp for the fill.
Request
GET/markets/{market}/trades
Click Try It
GET

User Orders

Returns one wallet's resting orders, optionally scoped to the Market field.

userWallet address in the path.
marketOptional engine market query param.
Request
GET/users/{user}/orders
Click Try It
GET

User Closed Orders

Returns this wallet's orders that ended without trading: cancelled, expired, or rejected. Filled orders appear in trades/history instead.

userWallet address in the path.
marketOptional engine market query param.
limitOptional, default 50, max 500.
Request
GET/users/{user}/orders/closed
Click Try It
GET

Order Status

Returns status for one order id. Use an id returned from POST /orders or copied from the current orders response.

orderIdGlobal engine order id, for example o104.
404Order id not found.
Request
GET/orders/{orderId}
Set Order id, then click Try It

Try POST /orders

Prerequisites For API-Only Trading

PRIVATE_KEYThe market maker's own wallet key. The API never creates signatures.
RPC_URLRPC for the chain where the CLOB market lives, for funding calls.
ENGINE_MARKETEngine label, for example evm:97:314.
CLOB_MARKET_IDNumeric on-chain market id, for example 314.
CLOB_EXCHANGEClobExchange contract address. This is the EIP-712 verifying contract.
PAYMENT_TOKENCollateral token used by this market, usually USDC for EVM markets.

Three Raw API Requests

This is the exact API flow a market maker bot performs after it has found the engine market label and generated a fresh order signature locally.

GET https://app-staging.tokenplay.ai/clob/v1/markets

POST https://app-staging.tokenplay.ai/clob/v1/orders
content-type: application/json

{
  "user": "0x87c088C6F5550516b8Ee80a9608149e140779850",
  "market": "evm:97:314",
  "action": "buy",
  "outcome": "YES",
  "price": 660,
  "size": "18181818",
  "type": "GTC",
  "salt": 1787155825206943,
  "expiry": 0,
  "signature": "0x<fresh_eip712_signature>"
}

GET https://app-staging.tokenplay.ai/clob/v1/markets/evm%3A97%3A314/book

GET https://app-staging.tokenplay.ai/clob/v1/markets/evm%3A97%3A314/orders

The docs button below cannot invent signature. Use the example client or an MM bot to sign first, then send the POST body.

Signature Generation Script — BSC Mainnet

Market makers generate a new signature locally for each unique order payload. The private key stays in their bot or vault; TokenPlay only receives the resulting signature.

EVM orders are EIP-712 typed data signed against the ClobExchange contract for that chain.

import { Wallet } from "ethers";

const wallet = new Wallet(process.env.PRIVATE_KEY);
const chainId = 97;
const clobMarketId = 314n;
const clobExchange = process.env.CLOB_EXCHANGE;

const domain = {
  name: "tokenplay-clob-order-v1",
  version: "1",
  chainId,
  verifyingContract: clobExchange,
};

const types = {
  Order: [
    { name: "user", type: "address" },
    { name: "market", type: "uint256" },
    { name: "action", type: "uint8" },
    { name: "outcome", type: "uint8" },
    { name: "price", type: "uint256" },
    { name: "size", type: "uint256" },
    { name: "salt", type: "uint256" },
    { name: "expiry", type: "uint256" },
  ],
};

const message = {
  user: wallet.address,
  market: clobMarketId,
  action: 0,        // 0 = buy, 1 = sell
  outcome: 0,       // 0 = YES, 1 = NO
  price: 660n,      // 66.0c
  size: 18181818n,  // 18.181818 shares
  salt: BigInt(Date.now()) * 1000n,
  expiry: 0n,
};

const signature = await wallet.signTypedData(domain, types, message);

console.log(JSON.stringify({
  user: wallet.address,
  market: "evm:97:314",
  action: "buy",
  outcome: "YES",
  price: Number(message.price),
  size: message.size.toString(),
  type: "GTC",
  salt: Number(message.salt),
  expiry: Number(message.expiry),
  signature,
}, null, 2));

POST /orders

This must contain a fresh wallet signature for this exact payload. EVM orders are EIP-712 typed data signed against ClobExchange. The API does not take payment by itself; collateral must already be funded/deposited on-chain by the trading flow.

signatureGenerated by the market maker script with wallet.signTypedData(domain, types, message). Do not copy a signature from an old GET /orders/{orderId} response.
marketAPI book label, for example evm:97:314.
message.marketNumeric on-chain market id inside the EIP-712 signed payload, for example 314.
priceTicks from 1 to 999. 660 means 66.0c.
sizeRaw share units. With 6 decimals, 18181818 means 18.181818 shares.
saltUnique per order. A new salt means a new signature.
After a successful POST, the returned order id is copied into the Order id field.
Click Send signed order

Try DELETE /orders/{orderId}

DELETE /orders/{orderId}

Cancel signature must be a wallet signature over: tokenplay-clob-cancel-v1|<user>|<orderId>.

This is an engine cancel, so there is no explorer transaction. Funds become idle/withdrawable in the CLOB exchange; use contract withdraw or npm run mm -- withdraw --amount <amount> to return them to the wallet.


        
Click Send cancel

Market Orders

This engine holds limit orders only. A market order does not rest in the book — it fills straight against the AMM pool, and that happens in the settlement bridge, a different service on a different base URL: https://app-staging.tokenplay.ai/clob-engine.

1. Quote the pool

GET /clob-engine/quote?market=373            ← Solana
GET /clob-engine/quote?chain=97&market=314      ← EVM

{ "market": 373, "yes": 655, "no": 345 }

Prices in ticks, same scale as an order: 1000 = $1.00. Use the side you are buying as your signed price limit, or a worse one to allow slippage — the fill is bounded by what you signed.

2. Send the signed order

POST /clob-engine/market-order
content-type: application/json

The order is signed exactly as a limit order — same message, same scheme. What differs is where you send it and one extra field.

EVMThe signed order plus chainId. market is the bare on-chain id.
Solanamarket, user, action, outcome, price, size, salt, expiry, signature, and optionally appMarketId. No chainId.

EVM, end to end

// The message is signed exactly as for POST /orders — same domain, same struct.
// market inside the SIGNATURE is always the bare id; it is a uint256 on chain.
const signature = await wallet.signTypedData(
  { name: "tokenplay-clob-order-v1", version: "1", chainId: 97, verifyingContract: EXCHANGE },
  { Order: [
      { name: "user", type: "address" }, { name: "market", type: "uint256" },
      { name: "action", type: "uint8" }, { name: "outcome", type: "uint8" },
      { name: "price", type: "uint256" }, { name: "size", type: "uint256" },
      { name: "salt", type: "uint256" }, { name: "expiry", type: "uint256" } ] },
  { user: wallet.address, market: 314n, action: 0, outcome: 0,
    price: 700n, size: 10000000n, salt: 1725440000000n, expiry: 0n }
);

await fetch("https://app-staging.tokenplay.ai/clob-engine/market-order", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    chainId: 97,          // picks the EVM adapter — omit it and the request goes to Solana
    market: 314,          // the ON-CHAIN id, NOT the engine label "evm:97:314"
    user: wallet.address,
    action: "buy", outcome: "YES",
    price: 700,           // a LIMIT, not a target: quote first, then sign some slippage room
    size: "10000000",
    salt: 1725440000000, expiry: 0,
    signature,
  }),
});

Or, with the bundled client: npm run mm -- quote then npm run mm -- market-order --action buy --outcome YES --price 700 --size 10000000.

The mistake worth naming. Every other endpoint on the engine speaks the label evm:97:314, so a body built by copying a working POST /orders call carries it here too — and the EVM adapter reads it as a number, gets NaN, and answers 422 bad market id. There is no marketId field on this route; the field is market. Both spellings are accepted now, but the bare id is the one to write.

The routing rule is the whole trick. The bridge serves both chain families on one set of paths and decides which adapter answers by asking one question: does the body have a chainId field? If yes it goes to EVM, if no it goes to Solana. A Solana order that carries chainId is handed to the EVM adapter and comes back as an unknown market; an EVM order without it goes to Solana and fails on a malformed pubkey. Neither error mentions routing.

Returns 200 with the fill, or 422 when the pool cannot fill it at your signed limit. Unlike POST /orders, nothing is left resting afterwards.

Money: Deposit, Withdraw, Liquidity, Claim

These move funds rather than read them, so the wallet must sign something. The flow differs by chain. Solana: the bridge builds an unsigned transaction through /tx/*, your wallet signs it, and you submit it. EVM: there is currently no /tx/* builder; call ClobExchange directly from the user's wallet/RPC, or use examples/market-maker/mm-client.mjs for testing.

First, make sure the market exists on chain

On EVM, this is the public preparation call the UI shows as Preparing market.... Run it before the first deposit/order on a fresh market; otherwise deposit() can revert MarketMissing. On Solana, the same route is operator-only because it spends authority SOL to create accounts.

POST https://app-staging.tokenplay.ai/clob-engine/ensure-market
content-type: application/json

{"chainId": 97, "market": 314, "closeTime": 0}    ← EVM, no token

POST https://app-staging.tokenplay.ai/clob-engine/ensure-market
x-clob-resolve-token: <operator token>
content-type: application/json

{"marketId": 880101}                              ← Solana operator pre-provision

Solana: build → sign → submit

// 1. ask the bridge to assemble the Solana transaction
const res = await fetch("https://app-staging.tokenplay.ai/clob-engine/tx/deposit", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ marketId: 880101, user: wallet.publicKey.toBase58(), amount: "100000000" }),
}).then(r => r.json());
// { ok: true, transaction: "<base64>", blockhash, lastValidBlockHeight, createsTokenAccount }

// 2. sign it yourself — the only step that touches your key
const tx = Transaction.from(Buffer.from(res.transaction, "base64"));
tx.sign(wallet);

// 3. send it, here or straight to your own RPC
await fetch("https://app-staging.tokenplay.ai/clob-engine/tx/submit", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ signedTransaction: tx.serialize().toString("base64") }),
});
// { ok: true, signature: "…", confirmed: true }

The /tx/* routes are Solana-only. Do not add chainId to these calls; a body with chainId is routed to the EVM adapter, which does not serve /tx/*. /tx/submit waits for confirmation before it answers. Reading an account straight after a bare send is how a working instruction looks like it did nothing.

Solana endpointBodyBuilds the instruction
POST /tx/depositmarketId, user, amountdeposit()
POST /tx/withdrawmarketId, user, amountwithdraw()
POST /tx/add-liquiditymarketId, user, amountaddLiquidity()
POST /tx/remove-liquiditymarketId, userremoveLiquidity()
POST /tx/claimmarketId, userclaim()
POST /tx/claim-lp-feesmarketId, userclaimLpFees()
POST /tx/submitsignedTransactionbroadcasting it yourself

For Solana /tx/*, amount is raw units in the payment mint's own decimals — USDC has 6, so 100000000 is $100. Same scale an order's size uses; no floats anywhere. A wallet that has never held the mint gets createsTokenAccount: true and the token account is opened inside the same transaction.

Claim is not withdraw

claim turns winning shares into free balance inside the exchange. withdraw is a second transaction that moves tokens to the wallet. Stopping after the first is the most common “where is my money”. remove-liquidity additionally needs the market resolved and the AMM pool to have claimed its own shares, or it reverts AmmNotSettled.

The same thing from a terminal

For trying it by hand before writing a client. EVM uses direct contract transactions through mm-client.mjs; Solana uses unsigned transaction builders or the local helper below.

# EVM, in prediction-backend
npm run mm -- ensure-market
npm run mm -- balance
npm run mm -- deposit --amount 25
npm run mm -- add-liquidity --amount 50
npm run mm -- claim-lp-fees
npm run mm -- remove-liquidity
npm run mm -- withdraw --amount <clobBalance>

# Solana, in tokenplay-clob-bridge
node scripts/solana-collateral.mjs balance 880101
node scripts/solana-collateral.mjs deposit 880101 100 --execute
node scripts/solana-collateral.mjs add-liquidity 880101 100 --execute
node scripts/solana-collateral.mjs claim 880101 --execute
node scripts/solana-collateral.mjs withdraw 880101 25 --execute

For EVM, claim is winnings only. LP exit is remove-liquidity, then withdraw. For Solana, solana-collateral.mjs is dry-run unless you pass --execute.

Solana Complete Run

Market 880101 on devnet, created and closed through the Solana /tx/* calls. Check your own run against the arithmetic: when a step is silently doing nothing, the totals stop adding up long before anything throws.

StepCallWalletExchange balanceShares
start$500.00
create the bookPOST /ensure-market$500.00
depositPOST /tx/deposit$400.00$100.00
rest an orderPOST /orders$400.00$100.00
cancel itDELETE /orders/{id}$400.00$100.00
buy 10 YES at 50¢POST /market-order$400.00$94.9510 YES
resolve YESPOST /resolve$400.00$94.9510 YES
claimPOST /tx/claim$400.00$104.950
withdrawPOST /tx/withdraw$504.95$0.000

Net $4.95 = a $10 payout on 10 winning shares, less the $5 they cost at 50¢, less $0.05 of fee. The fee is 1% of the trade.

Solana Walkthrough

The same Solana run as above, as commands. Paste them in order against a market of your own. The build steps never touch your private key; the signing pipe is the wallet side.

Setup

export ENGINE=https://app-staging.tokenplay.ai/clob/v1
export BRIDGE=https://app-staging.tokenplay.ai/clob-engine
export USER_PUB=<your base58 pubkey>
export M=880201                # the numeric market id

Amounts below are raw units: USDC has 6 decimals, so 50000000 is $50. Prices are ticks: 1000 = $1.00.

1 — Bring the market up, and look at it

curl -s -X POST $BRIDGE/ensure-market   -H "x-clob-resolve-token: $TOKEN" -H 'content-type: application/json'   -d "{"marketId":$M}"
# {"ok":true,"market":880201}

curl -s "$BRIDGE/quote?market=$M"
# {"market":880201,"yes":500,"no":500}

curl -s $ENGINE/markets/$M
curl -s $ENGINE/markets/$M/book
curl -s $ENGINE/markets/$M/ticker

The first call is slow: it creates the accounts and funds the AMM. It is idempotent, so run it again freely.

You almost certainly do not need this step, or the token. x-clob-resolve-token is an operator secret — the same one that gates /propose and /resolve, which decide who gets paid — and it is not issued to integrators. A market published through the app is normally brought up on chain before you get to it; reading /quote for it will tell you. If a market really has no book, ask an operator to run this one call for it. Everything else below needs nothing but your own wallet.

2 — Deposit  signs

# build — see what comes back before piping it anywhere
curl -s -X POST $BRIDGE/tx/deposit -H 'content-type: application/json'   -d "{"marketId":$M,"user":"$USER_PUB","amount":"50000000"}"
# {"ok":true,"transaction":"<base64>","blockhash":"…","createsTokenAccount":false}

# build → sign → submit, in one pipe
curl -s -X POST $BRIDGE/tx/deposit -H 'content-type: application/json'   -d "{"marketId":$M,"user":"$USER_PUB","amount":"50000000"}" | jq -r .transaction | node sign.mjs | curl -s -X POST $BRIDGE/tx/submit -H 'content-type: application/json' --data-binary @-
# {"ok":true,"signature":"…","confirmed":true}

sign.mjs is the whole of your side of this. It ships as scripts/sign.mjs in tokenplay-clob-bridge, and it is short enough to paste:

import { Transaction, Keypair } from "@solana/web3.js";
import bs58 from "bs58";

const wallet = Keypair.fromSecretKey(bs58.decode(process.env.USER_KEY));

let b64 = "";
process.stdin.setEncoding("utf8");
for await (const chunk of process.stdin) b64 += chunk;
b64 = b64.trim();

if (!b64 || b64 === "null") {
  // jq prints "null" when the build step returned an error instead.
  console.error("no transaction on stdin — run the /tx/... call alone and read its error");
  process.exit(1);
}

const tx = Transaction.from(Buffer.from(b64, "base64"));
tx.sign(wallet);
process.stdout.write(JSON.stringify({ signedTransaction: tx.serialize().toString("base64") }));

Run it as USER_KEY=<base58 secret key> node sign.mjs. This is a testing convenience: a real integration signs with the user's wallet — Phantom, a Ledger, a custodial signer — and the code is the same three lines, Transaction.from, sign, serialize. Nothing about the transaction is secret; only the signing is.

3 — Rest an order, then cancel it  signs

npm run mm:solana -- place --action buy --outcome YES --price 400 --size 5000000
# HTTP 201  {"accepted":true,"order":{"id":"o2","status":"open",…}}

curl -s $ENGINE/markets/$M/book
curl -s "$ENGINE/users/$USER_PUB/orders?market=$M"

export OID=$(curl -s $ENGINE/markets/$M/orders | jq -r '.orders[0].id')
curl -s $ENGINE/orders/$OID
# {"order":{"id":"o2","status":"open","remaining":"5000000"}}   ← no salt, no signature

npm run mm:solana -- cancel --id $OID
curl -s "$ENGINE/users/$USER_PUB/orders/closed?market=$M"

Swap place for message to print the signed string and the signature without sending anything — the fastest way to see why a signature is being rejected.

4 — Prove the signature is really checked

curl -s -X POST $ENGINE/orders -H 'content-type: application/json' -d "{
  "user":"$USER_PUB","market":"$M","action":"buy","outcome":"YES",
  "price":500,"size":"1000000","type":"GTC","salt":42,"expiry":0,
  "signature":"<any other valid base58 signature>"}"
# 422  {"accepted":false,"reason":"invalid signature"}

5 — Trade against the AMM  signs

curl -s -X POST $BRIDGE/market-order -H 'content-type: application/json'   -d '{"market":880201,"user":"<pubkey>","action":"buy","outcome":"yes",
       "price":700,"size":5000000,"salt":123,"expiry":0,"signature":"<base58>"}'
# {"ok":true,"sig":"…","execPrice":500}

curl -s "$BRIDGE/quote?market=$M"
# {"yes":667,"no":333}    ← the AMM sold YES, so YES got dearer

Your signed price is a limit, not a request: 700 here means “fill me at 70¢ or better”, and it filled at 50¢. A 422 means the pool could not do it within your limit.

6 — Liquidity, resolution, payout  signs

curl -s -X POST $BRIDGE/tx/add-liquidity -H 'content-type: application/json'   -d "{"marketId":$M,"user":"$USER_PUB","amount":"25000000"}" | jq -r .transaction | node sign.mjs | curl -s -X POST $BRIDGE/tx/submit -H 'content-type: application/json' --data-binary @-

curl -s -X POST $BRIDGE/resolve -H "x-clob-resolve-token: $TOKEN"   -H 'content-type: application/json' -d "{"marketId":$M,"outcome":"YES"}"
# {"ok":true,"txHash":"…","ammTx":"…"}      ← ammTx null means the pool held no shares

for k in claim claim-lp-fees remove-liquidity; do
  curl -s -X POST $BRIDGE/tx/$k -H 'content-type: application/json'     -d "{"marketId":$M,"user":"$USER_PUB"}"   | jq -r .transaction | node sign.mjs   | curl -s -X POST $BRIDGE/tx/submit -H 'content-type: application/json' --data-binary @-
done

7 — Withdraw  signs

# the balance after a claim is never a round number — read it, do not guess
curl -s -X POST $BRIDGE/tx/withdraw -H 'content-type: application/json'   -d "{"marketId":$M,"user":"$USER_PUB","amount":"76975000"}" | jq -r .transaction | node sign.mjs | curl -s -X POST $BRIDGE/tx/submit -H 'content-type: application/json' --data-binary @-

Leaving a placeholder in amount makes the build return an error, jq print null, and the signer choke on nothing. If a pipe dies oddly, run the /tx/ call alone and read its error.

Check the arithmetic, not just the status codes

Deposit $50, buy 5000000 at an exec price of 500, and the position should read $47.475: the trade cost $2.50 of notional and $0.025 of fee. After YES resolves, claim adds $5.00 — the shares are worth par — for $52.475. Removing $25 of liquidity returns $24.50, not $25: the pool sold shares for $2.50 that settled at $5.00, and your 25 of its 125 shares carry 20% of that $2.50 loss. A run where every call returns 200 and the totals do not close is a run that has quietly failed.

Rate Limits

API Rate Limits

Defaults are intentionally generous and configurable per environment.

CLOB_API_RATE_LIMIT_PUBLICDefault 600 requests per window for reads and docs-backed API calls.
CLOB_API_RATE_LIMIT_ORDERSDefault 120 order write requests per window.
CLOB_API_RATE_LIMIT_WINDOW_MSDefault 60000.