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": "0xb09b789f98ad9cc9e2741a8ce8e15144fbbbe884",
"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. A 2-team moneyline is deployed as a mirror pair. Quote only the market whose
canonical_sibling_market_idequals its ownid; opposite-side flow matches on that single book. Quoting both siblings splits your liquidity. (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 are { price, size, num_orders }. See Orders: Get Order Book. For live updates, subscribe over WebSocket instead of polling.
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
Open a WebSocket to wss://api.4rho.com/ws. Authenticate with a streaming ticket: call POST /v1/auth/ws-ticket (over normal HMAC, scope read:account) to mint a single-use wst_… ticket (valid <=60s), open the socket, and send it as the first frame:
{ "type": "auth", "token": "wst_…" }
(Signing the HMAC headers onto the upgrade request only works when connecting directly to the origin - the production edge strips them, so the ticket flow is the path that works everywhere.) Then 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.