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, maker and signer are 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_id equals its own id; 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/orders rejects with 422 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-rate and 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 (needs stream:market) streams trade / book_delta / bbo so you re-quote on book moves.
  • user: true (needs stream:user) streams your private fill and order_status events.

8. Re-quote and protect

  • Re-quote: POST /v1/orders/cancel-replace atomically swaps one resting order for a freshly-signed one. Amount changes always need a new signature - use new_order, never the legacy bare-amount amend.
  • Unwind fast: DELETE /v1/orders/cancel-all is the kill switch; DELETE /v1/orders/cancel-market scopes it to one market; DELETE /v1/orders/batch cancels up to 200 by hash.
  • Dead-man's switch: POST /v1/orders/heartbeat once 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 - carries available_balance_usdc for sizing.

All require read:account. See User Data.