DEV Community

Cover image for API Design Patterns from the World's Largest Prediction Market: Lessons from Polymarket
Hassann
Hassann

Posted on • Originally published at apidog.com

API Design Patterns from the World's Largest Prediction Market: Lessons from Polymarket

Prediction market APIs are hard to design because they combine expiring financial instruments, real-time probability pricing, multi-outcome capital relationships, human users, and automated trading bots. Every API decision gets tested under latency, security, and correctness pressure.

Try Apidog today

Polymarket, one of the largest prediction market platforms by volume, is useful to study because its API is not just CRUD over markets and orders. It separates discovery, trading, analytics, authentication, signed orders, and real-time updates into distinct surfaces.

This article extracts eight implementation patterns you can apply when designing APIs for trading systems, crypto apps, fintech products, or any domain where state, trust, and data semantics matter.


Pattern 1: Separate APIs by domain, not by database entity

Polymarket exposes three main APIs:

  • Gamma API (gamma-api.polymarket.com) — market discovery, events, tags, search
  • CLOB API (clob.polymarket.com) — order book data, pricing, order placement
  • Data API (data-api.polymarket.com) — user positions, trades, analytics, leaderboards

Each API has a different purpose:

API Primary use Auth model Consumer
Gamma API Browse and discover markets Public Apps, users, indexers
CLOB API Read books and place orders Public reads, authenticated writes Traders, bots, market makers
Data API Query wallet-based activity Public, address-scoped Dashboards, analytics tools

A less deliberate design would put everything under one API:

/api/markets
/api/orders
/api/users
/api/trades
Enter fullscreen mode Exit fullscreen mode

Polymarket instead separates by operational domain:

https://gamma-api.polymarket.com
https://clob.polymarket.com
https://data-api.polymarket.com
Enter fullscreen mode Exit fullscreen mode

That separation matters because discovery, trading, and analytics have different requirements:

  • Discovery optimizes for searchability and browsing.
  • Trading optimizes for correctness, latency, and authentication.
  • Analytics optimizes for historical reads and wallet-level aggregation.

Implementation takeaway

When designing your API, start with usage boundaries instead of tables.

Ask:

Who calls this API?
How often do they call it?
Does it need authentication?
Does it need low latency?
Can it scale independently?
Can it fail independently?
Enter fullscreen mode Exit fullscreen mode

If the answers differ significantly, consider separate API surfaces.


Pattern 2: Make read access public when data liquidity matters

Polymarket makes market data public:

curl "https://gamma-api.polymarket.com/events?limit=5"
Enter fullscreen mode Exit fullscreen mode

No API key is required for basic market discovery.

That includes data such as:

  • Event metadata
  • Market metadata
  • Prices
  • Order books
  • Historical trades

This is a deliberate platform decision. Traditional financial exchanges often monetize market data directly. Polymarket treats market data as infrastructure: the more people can read it, analyze it, and build on it, the more useful the market becomes.

Implementation takeaway

Separate read access from write access.

A common mistake is to require authentication for everything:

GET /markets       requires auth
GET /order-book    requires auth
POST /orders       requires auth
Enter fullscreen mode Exit fullscreen mode

For many platforms, this creates unnecessary friction. A better model is:

GET /markets       public
GET /order-book    public
GET /trades        public
POST /orders       authenticated
DELETE /orders     authenticated
Enter fullscreen mode Exit fullscreen mode

Public reads are especially useful when:

  • Data consumers vastly outnumber writers.
  • Developers need to explore before integrating.
  • Bots, dashboards, and indexers increase platform value.
  • The sensitive action is mutation, not observation.

Add authentication at the point where risk appears: placing orders, moving funds, changing state, or accessing private account information.


Pattern 3: Use different authentication levels for different trust levels

Trading endpoints require authentication, but Polymarket uses two authentication levels with different responsibilities.

L1 authentication: prove wallet ownership

L1 authentication uses an EIP-712 signature from the user’s private key. It proves that the caller controls the wallet.

You use it to create or derive API credentials:

// L1: Use your private key to derive API credentials
const credentials = await client.createOrDeriveApiKey();

// Example result:
// {
//   key: "...",
//   secret: "...",
//   passphrase: "..."
// }
Enter fullscreen mode Exit fullscreen mode

This is a high-trust action. It should require the strongest credential: the private key signature.

L2 authentication: sign each API request

After API credentials exist, routine trading requests use HMAC-SHA256 headers:

{
  "POLY_ADDRESS": "0x...",
  "POLY_SIGNATURE": "<hmac-sha256>",
  "POLY_TIMESTAMP": "1716000000",
  "POLY_API_KEY": "550e8400-...",
  "POLY_PASSPHRASE": "..."
}
Enter fullscreen mode Exit fullscreen mode

L2 authentication proves that a specific request came from the credential holder without requiring a private-key signature on every API call.

Implementation takeaway

Do not use the same authentication ceremony for every action.

A practical model:

Operation Auth strength
Create API key Strong identity proof
Rotate credentials Strong identity proof
Place order Request signature/session credential
Read public market data No auth
Read private account data Session credential
Withdraw funds Strong identity proof

This maps beyond crypto. In a traditional app:

  • L1 is “prove you are the account owner.”
  • L2 is “prove this request came from an active authorized session.”

That distinction improves both security and usability.


Pattern 4: Treat high-stakes actions as signed payloads, not just API calls

On Polymarket, placing an order is not merely sending JSON to a server. The order is a cryptographically signed financial instruction.

Example order:

const response = await client.createAndPostOrder(
  {
    tokenID: "71321045679...",
    price: 0.65,
    size: 100,
    side: Side.BUY,
  },
  {
    tickSize: "0.01",
    negRisk: false,
  },
  OrderType.GTC
);
Enter fullscreen mode Exit fullscreen mode

Under the hood, the SDK creates an EIP-712 typed data structure, signs it with the user’s private key, and submits the signed order. The matching engine runs offchain, but matched trades settle on Polygon using those signatures.

The important design point: the operator cannot fabricate trades or move funds without user authorization. The signed message is the authorization.

Conventional API semantics

In a normal API, this means:

Please perform this action for me.
Enter fullscreen mode Exit fullscreen mode

Example:

POST /orders
Authorization: Bearer <token>
Enter fullscreen mode Exit fullscreen mode

The server decides whether to execute the action.

Signed-message semantics

With signed orders, the payload means:

Here is a signed instruction authorizing this exact action.
Enter fullscreen mode Exit fullscreen mode

The API acts more like a relay than an authority.

Implementation takeaway

For high-stakes operations, consider making the payload itself verifiable.

Useful domains include:

  • Financial transactions
  • Legal approvals
  • Contract signatures
  • Permission grants
  • Sensitive workflow approvals
  • Cross-system authorization

Instead of relying only on transport-layer credentials, encode authorization into the payload:

{
  "action": "transfer",
  "amount": "100.00",
  "asset": "USDC",
  "recipient": "0x...",
  "expiresAt": "2026-01-01T00:00:00Z",
  "signature": "0x..."
}
Enter fullscreen mode Exit fullscreen mode

This gives you better auditability, non-repudiation, and replay protection when designed correctly.


Pattern 5: Encode the domain ontology in the API model

Polymarket models prediction markets using two important objects:

  • Event
  • Market

An Event is the broader question:

“Who will win the 2026 US Senate race in Pennsylvania?”

A Market is a specific tradable binary outcome inside that event:

“Will Bob Casey win?”

One event can contain many markets.

Example structure:

{
  "id": "501",
  "title": "2026 Pennsylvania Senate Race",
  "negRisk": true,
  "markets": [
    {
      "id": "2301",
      "question": "Will Bob Casey win?",
      "outcomePrices": "[\"0.42\", \"0.58\"]"
    },
    {
      "id": "2302",
      "question": "Will Dave McCormick win?",
      "outcomePrices": "[\"0.35\", \"0.65\"]"
    },
    {
      "id": "2303",
      "question": "Will a third candidate win?",
      "outcomePrices": "[\"0.23\", \"0.77\"]"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This distinction is not cosmetic. It tells API consumers how the domain works.

The event groups related markets. The market represents a tradable outcome. The negRisk flag signals that markets inside the event have capital relationships.

Implementation takeaway

Avoid flattening important domain concepts into generic resources.

A weak model might expose only:

GET /markets
Enter fullscreen mode Exit fullscreen mode

A stronger model exposes relationships:

GET /events
GET /events/:id/markets
GET /markets/:id
Enter fullscreen mode Exit fullscreen mode

If the distinction matters to business logic, it should exist in the API.

Good domain modeling helps clients avoid incorrect assumptions. For example, if an automated trader ignores negRisk: true, it may construct the wrong position model.

Your API should make these relationships visible instead of hiding them in documentation.


Pattern 6: Represent domain invariants as API fields

The negRisk flag is one of Polymarket’s most interesting design choices.

In a standard multi-outcome event, each market can be treated independently. But in a NegRisk event, exactly one outcome can win. That creates mathematical relationships between positions:

1 No token on outcome A ≡ 1 Yes token on every other outcome

Example:

Before After
1× No (Other) 1× Yes (Casey) + 1× Yes (McCormick)

This is not just theoretical. It affects trading and settlement behavior.

Polymarket exposes this through API fields:

{
  "negRisk": true
}
Enter fullscreen mode Exit fullscreen mode

And when placing orders, the client must pass the correct market options:

const response = await client.createAndPostOrder(
  {
    tokenID: "71321045679...",
    price: 0.65,
    size: 100,
    side: Side.BUY,
  },
  {
    tickSize: "0.01",
    negRisk: true
  },
  OrderType.GTC
);
Enter fullscreen mode Exit fullscreen mode

If the client gets this wrong, the order can be rejected or handled incorrectly.

Implementation takeaway

If your domain has hard rules, encode them as typed fields.

Do not leave critical invariants only in prose documentation.

Examples:

{
  "requiresKyc": true,
  "settlementMode": "on_chain",
  "isMutuallyExclusive": true,
  "minCollateralRatio": "1.50",
  "supportsPartialFill": true,
  "expiresAt": "2026-01-01T00:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Fields like these are valuable because clients can branch on them programmatically.

Documentation explains the rule. The API should expose the rule.


Pattern 7: Treat changing market parameters as state, not configuration

Many financial APIs treat tick size as static. Polymarket exposes tick size as dynamic market state.

When a market price approaches the extremes, above 0.96 or below 0.04, the minimum tick size narrows from 0.01 to 0.001.

Example WebSocket event:

{
  "event_type": "tick_size_change",
  "asset_id": "65818619657...",
  "old_tick_size": "0.01",
  "new_tick_size": "0.001",
  "timestamp": "100000000"
}
Enter fullscreen mode Exit fullscreen mode

The reason is practical. Near extreme probabilities, a 1-cent tick is too coarse. Moving from 0.04 to 0.03 is a large relative move. A smaller tick allows prices like 0.973 instead of forcing 0.97.

Implementation takeaway

Do not assume market parameters are static.

For trading clients, tick size should be part of the current market state:

type MarketState = {
  assetId: string;
  bestBid: string;
  bestAsk: string;
  tickSize: string;
};
Enter fullscreen mode Exit fullscreen mode

When a tick_size_change event arrives, update local state:

function handleTickSizeChange(event: {
  asset_id: string;
  new_tick_size: string;
}) {
  marketState[event.asset_id].tickSize = event.new_tick_size;
}
Enter fullscreen mode Exit fullscreen mode

Then validate orders against the current tick size before submitting:

function isValidPrice(price: number, tickSize: number) {
  return Number.isInteger(price / tickSize);
}
Enter fullscreen mode Exit fullscreen mode

If your client hard-codes tick size, it will eventually submit invalid orders.

The broader principle: changing domain state should be broadcast explicitly, not discovered only through failed requests.


Pattern 8: Use separate WebSocket layers for different real-time consumers

Polymarket runs two separate WebSocket systems.

Market Channel

The Market Channel is designed for trading consumers:

wss://ws-subscriptions-clob.polymarket.com/ws/market
Enter fullscreen mode Exit fullscreen mode

It streams data such as:

  • Order book snapshots
  • Price changes
  • Trade executions
  • Tick size changes

Subscription example:

{
  "assets_ids": [
    "65818619657568813474341868652308942079804919287380422192892211131408793125422"
  ],
  "type": "market"
}
Enter fullscreen mode Exit fullscreen mode

This channel is optimized around asset IDs and low-latency trading workflows.

Real-Time Data Socket

The Real-Time Data Socket serves a different use case:

wss://ws-live-data.polymarket.com
Enter fullscreen mode Exit fullscreen mode

It streams broader platform activity, including comments, crypto prices, equity prices, and social interaction events.

Subscription example:

{
  "action": "subscribe",
  "subscriptions": [
    {
      "topic": "crypto_prices",
      "type": "update",
      "filters": "btcusdt,ethusd"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

These consumers have different needs.

A market maker needs low-latency order book updates. A UI showing platform activity needs comments, prices, and social events. Combining both into one WebSocket system would force one infrastructure layer to serve conflicting requirements.

Implementation takeaway

Separate real-time infrastructure when consumers differ by:

  • Latency requirements
  • Message volume
  • Failure tolerance
  • Data shape
  • Subscription model
  • Operational priority

A practical split might look like this:

/ws/trading        low latency, order books, fills
/ws/activity       comments, notifications, social events
/ws/analytics      aggregates, leaderboards, dashboards
Enter fullscreen mode Exit fullscreen mode

Trying to make one WebSocket endpoint serve every use case usually creates unnecessary complexity and uneven performance.


What these patterns have in common

Polymarket’s API design makes the domain structure visible.

The main patterns are:

  1. Separate APIs by operational domain.
  2. Make public read access easy when data liquidity matters.
  3. Use different authentication levels for different trust levels.
  4. Represent high-stakes actions as signed payloads.
  5. Encode the domain ontology in the API model.
  6. Surface domain invariants as explicit fields.
  7. Treat changing parameters as real-time state.
  8. Split WebSocket infrastructure by consumer profile.

The broader design lesson: do not abstract away distinctions that matter.

If a concept affects client behavior, put it in the API. If a rule affects correctness, expose it as a field. If state changes over time, broadcast the change. If different consumers have different performance requirements, give them different interfaces.

Good API design is not only about clean routes and consistent naming. It is about making the system’s real constraints understandable and programmable.

Top comments (0)