Market-Maker Integration in 10 Minutes
An end-to-end path for a programmatic market maker: from an API key to resting two-sided quotes with a dead-man's switch. Each step links to the full reference. The load-bearing rule is step 4: sign the exact canonical fee or your orders never fill.
What you need
- An API key from a 4rho admin with scopes:
read:account,trade:orders,trade:bulk,stream:market,stream:user. Key id, secret, and passphrase are shown once (see Authentication). - An Ethereum private key for the wallet that will make markets. For a pure MM,
makerandsignerare both this wallet's address. It needs on-chain USDC + outcome-token balances (and approvals) to back the orders you sign. - Every request is HMAC-signed. Use signing version 2 (
X-4RHO-SIGNING-VERSION: 2, a nonce on every request). Full algorithm + copy-paste helpers: Authentication.
1. Discover the contract to sign against
GET /v1/platform/config
Never hard-code the Exchange address - it rotates on every contract cutover and a stale value makes every order fail INVALID_SIGNATURE. Read it at startup and re-read on any INVALID_SIGNATURE.
{
"chain_id": 137,
"exchange_address": "0x7dcacee62615f0f53dc7b82e1e2fc83cd92ad7e7",
"usdc_address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
"ctf_address": "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
}
exchange_address is your EIP-712 verifyingContract; chain_id is the domain chainId (137, Polygon mainnet). See EIP-712 Signing.
2. Find a market to quote
GET /v1/markets
Each market carries id, yes_token_id (outcome 0), no_token_id (outcome 1), deploy_status (quote only deployed markets), fee_bps, and canonical_sibling_market_id. Full field list: Platform: List Markets.
Mirror moneylines - quote one sibling. For a designated mirror pair, quote both sides of the market whose
canonical_sibling_market_idequals its ownid, not across the siblings' separate books. A missing hint does not prove the market is standalone or eligible to quote: annotation can be unavailable. See Mirror moneylines for the shared designation and freshness contract. (The Events (SSE) page is an admin dashboard feed, not a discovery endpoint - use/v1/markets.)
3. Read the order book
GET /v1/orders/book/:market_id?both=true
both=true returns outcome_0 and outcome_1 in one response so you price both
sides from a single rate-limited call. Levels carry price, num_orders and
explicit token_quantity_atomic / notional_usdc_micro decimal strings. Missing
canonical amounts mean unknown depth. Do not use deprecated size, which mixes
maker denominations on synthetic asks. See
Orders: Get Order Book.
Subscribe over WebSocket for order-change hints, coalesce
them into canonical depth reads, and retain periodic recovery. A book_delta
describes one order, not the complete quantity at a price; it cannot replace a
level or prove queue priority.
4. Read the fee and sign it into the order
GET /v1/market-data/:market_id/fee-rate
{ "market_id": "...", "maker_fee_bps": 50, "taker_fee_bps": 50, "maker_fee_pct": "0.5%", "taker_fee_pct": "0.5%" }
Sign maker_fee_bps into feeRateBps. Today that is 50 platform-wide, but read the endpoint - the value is enforced two ways:
- Sign the wrong value ->
POST /v1/ordersrejects with422 fee_rate_bps mismatch(the canonical rate is in the message; re-sign and retry). - If the canonical fee changes while your order rests, the matcher silently skips the stale resting order (#3672). Re-read
/fee-rateand re-sign whenever your effective fee moves.
Do not sign 0, and do not sign platform_fee_bps. Full rule: Signing the correct fee.
5. Sign the EIP-712 order
Build the 12-field Order struct (field order is load-bearing), sign it with your wallet key against the domain from step 1, and set taker to the zero address (directed orders are rejected). For a maker order, feeRateBps = the maker_fee_bps from step 4. Working ethers.js / web3.py examples: EIP-712 Signing.
order = {
"salt": random_uint256(),
"maker": wallet.address,
"signer": wallet.address,
"taker": "0x0000000000000000000000000000000000000000", # required
"tokenId": int(market["yes_token_id"]), # outcome 0
"makerAmount": 50_000_000, # you give 50 USDC (6 decimals)
"takerAmount": 100_000_000, # you want 100 outcome tokens -> price 0.50
"expiration": 0, # 0 = GTC, or a unix ts for GTD
"nonce": 0,
"feeRateBps": 50, # == maker_fee_bps from /fee-rate
"minTakerNet": 0,
"side": 0, # 0 = BUY, 1 = SELL
}
# signature = wallet.sign_typed_data(domain, {"Order": ORDER_TYPES}, order)
6. Post quotes in a batch
POST /v1/orders/batch
Place up to 15 orders per call (scope trade:bulk). Your API-key tier limit replaces the per-endpoint defaults - a market_maker key gets 100 req/s on single POST /v1/orders too (see Rate Limits) - but batching still moves 15 orders per request, so it is the recommended way to quote. Each row succeeds or fails independently. Body + result shape: Batch Operations.
{ "orders": [ { "market_id": "...", "outcome_index": 0, "side": "BUY", "token_id": "...", "maker_amount": "50000000", "taker_amount": "100000000", "maker": "0x...", "signer": "0x...", "taker": "0x0000000000000000000000000000000000000000", "salt": "...", "nonce": 0, "expiration": 0, "fee_rate_bps": 50, "signature": "0x...", "time_in_force": "GTC" } ] }
7. Stream your fills and the book
API-key clients authenticate a WebSocket upgrade directly with the signed HMAC headers documented in the WebSocket guide, including a fresh X-4RHO-NONCE. Do not send a raw access JWT or mint a browser streaming ticket: API keys cannot mint that user-like credential because it would discard their exact key identity and scopes. HMAC upgrade authentication requires a direct-origin endpoint; the production edge strips these headers, so wss://api.4rho.com/ws exposes only public market channels to API-key clients routed through that edge. Once authenticated at the origin, subscribe:
{ "channel": "subscribe", "data": { "markets": ["market-id-1"], "user": true } }
markets(needsstream:market) streamstrade/book_delta/bboso you re-quote on book moves.user: true(needsstream:user) streams your privatefillandorder_statusevents.
8. Re-quote and protect
- Re-quote:
POST /v1/orders/cancel-replaceatomically swaps one resting order for a freshly-signed one. Amount changes always need a new signature - usenew_order, never the legacy bare-amount amend. - Unwind fast:
DELETE /v1/orders/cancel-allis the kill switch;DELETE /v1/orders/cancel-marketscopes it to one market;DELETE /v1/orders/batchcancels up to 200 by hash. - Dead-man's switch:
POST /v1/orders/heartbeatonce a second. Miss it for 45 seconds and every open order is auto-cancelled - your protection against a disconnected bot leaving stale quotes on the book.
Positions and balance
GET /v1/user/positions- current outcome-token positions.GET /v1/user/trades/v2- your fill history.GET /v1/user/portfolio/pnl-24h- carriesavailable_balance_usdcfor sizing.
All require read:account. See User Data.