Welcome to API Academy

Module 16 · Production · ~25 min

Arbitrage.

By the end of this module, your bot can spot prices that don’t add up, and close them before someone else does. Real money for noticing what the rest of the market hasn’t yet.

To get there, you’ll automate detection and execution of parity mispricings, the canonical “risk-free” trade, once you accept that it isn’t.

Production tier · Reference card
Quick answer

How does an arbitrage bot work on a prediction market?

It watches for prices that violate an identity, the cleanest being intra-market parity: on a binary market YES + NO must sum to $1, so when bid(YES) + bid(NO) exceeds $1 you sell both sides, and when ask(YES) + ask(NO) is under $1 you buy both. The bot streams orderbookUpdate events from the WebSocketClient across a watch-list, recomputes both sums on every book delta, and only acts when the gap clears fees on both legs plus a buffer (the threshold is 1 + 2×FEE + BUFFER, with a 20 bps fee per leg and a 30 bps buffer in the example). Execution sends both legs as OrderType.FOK orders with unique clientOrderIds, so each leg fills completely or not at all, then confirms via POST /orders/status/batch. It’s the canonical “risk-free” trade, once you accept that it isn’t.

Endpoints verified 2026-06-09 against the OpenAPI spec.

Section 01

Opportunity classes.

Arbitrage on a prediction market has a handful of distinct flavours. Each class has its own detection logic, execution footprint, and failure modes.

Intra-market parity

On a binary market, YES + NO must sum to $1. When bid(YES) + bid(NO) > $1, sell both. When ask(YES) + ask(NO) < $1, buy both. Cleanest possible arb, two legs on one platform.

Legs: 2 · venue: Limitless · risk: minimal

Cross-platform

The same event listed on two venues at different prices. Buy on the cheaper side, sell on the richer. Carry risk while you bridge capital or wait for resolution.

Legs: 2 · venue: Limitless + other · risk: bridge, settlement mismatch

Correlated markets

Logically related markets whose implied probabilities contradict each other (for example, a “wins election” market and a set of individual state markets that must sum consistently). Not a risk-free arb, a statistical one.

Legs: N · venue: Limitless · risk: model assumption

Section 02

Detection loop.

A streaming detection loop over the orderbookUpdate event from WebSocketClient. For each book delta we recompute bid(YES) + bid(NO) and flag anything that exceeds $1 plus our fee + buffer threshold. Resubscribe on reconnect, the server doesn’t persist subscriptions.

How to run this

  1. Set LIMITLESS_API_KEY, this is a read-only stream, no private key required. Point WATCHED at the slugs you actually want to monitor.
  2. Save the snippet above as cross-market-arb.ts, then run npx tsx cross-market-arb.ts.
  3. The process sits silent most of the time, parity normally holds. When a book prints a SELL-BOTH arb or BUY-BOTH arb line with a slug and a number, you’ve caught a live mispricing outside your fee + 30 bps buffer.
// Module 16, Intra-market parity detection over a websocket stream.

import { WebSocketClient } from '@limitless-exchange/sdk';

const FEE    = 0.002;  // 20 bps per fill, per leg
const BUFFER = 0.003;  // require 30 bps edge after fees
const THRESH = 1 + 2 * FEE + BUFFER;

const WATCHED = ['btc-100k-weekly', 'eth-5k-weekly'];

const ws = new WebSocketClient({
  url:           'wss://ws.limitless.exchange',
  apiKey:        process.env.LIMITLESS_API_KEY,
  autoReconnect: true,
});
ws.connect();

// Subscriptions are *replaced* on each call, pass every slug in one go.
const resubscribe = () =>
  ws.subscribe('subscribe_market_prices', { marketSlugs: WATCHED });

ws.on('reconnect', resubscribe);
resubscribe();

ws.on('orderbookUpdate', (update) => {
  const book   = update.orderbook;
  const yesBid = Number(book?.yes?.bids?.[0]?.price ?? 0);
  const noBid  = Number(book?.no?.bids?.[0]?.price  ?? 0);
  const yesAsk = Number(book?.yes?.asks?.[0]?.price ?? 0);
  const noAsk  = Number(book?.no?.asks?.[0]?.price  ?? 0);

  const sellBoth = yesBid + noBid;   // sell YES + sell NO ⇒ receive
  const buyBoth  = yesAsk + noAsk;   // buy  YES + buy  NO ⇒ pay

  if (sellBoth > THRESH) {
    console.log('SELL-BOTH arb:', update.marketSlug, sellBoth.toFixed(4));
    // TODO: hand off to execute()
  }
  if (buyBoth < 2 - THRESH) {
    console.log('BUY-BOTH arb: ', update.marketSlug, buyBoth.toFixed(4));
    // TODO: hand off to execute()
  }
});

Section 03

Execution.

A two-leg arb has to execute atomically, or not at all. Submit both legs as OrderType.FOK with unique clientOrderIds for idempotency, duplicates return 409 Conflict. FOK fills the full makerAmount or rejects, so there’s no half-filled state to roll back. Confirm both legs with POST /orders/status/batch.

// Module 16, Two-leg FOK execution with idempotent client order ids.

import {
  HttpClient,
  MarketFetcher,
  OrderClient,
  Side,
  OrderType,
  ApiError,
} from '@limitless-exchange/sdk';
import { randomUUID } from 'node:crypto';

async function executeSellBoth(
  httpClient: HttpClient,
  orders: OrderClient,
  marketFetcher: MarketFetcher,
  slug: string,
  makerAmount: number,
) {
  const market  = await marketFetcher.getMarket(slug);
  const yesId   = market.positionIds[0];
  const noId    = market.positionIds[1];
  const keyBase = randomUUID();
  const legAId  = `${keyBase}-yes`;
  const legBId  = `${keyBase}-no`;

  const submitLeg = (tokenId: string, clientOrderId: string) =>
    orders.createOrder({
      marketSlug:  slug,
      tokenId,
      side:        Side.SELL,         // sell both sides for > $1 parity
      makerAmount,
      orderType:   OrderType.FOK,
      clientOrderId,
    }).catch((err) => {
      // 409 = duplicate clientOrderId; safe to treat as already-submitted.
      if (err instanceof ApiError && err.status === 409) return null;
      throw err;
    });

  const [legA, legB] = await Promise.allSettled([
    submitLeg(yesId, legAId),
    submitLeg(noId,  legBId),
  ]);

  // Confirm settlement via POST /orders/status/batch (1–50 items).
  const statusResp = await httpClient.post('/orders/status/batch', {
    items: [{ clientOrderId: legAId }, { clientOrderId: legBId }],
  });

  for (const r of statusResp.results) {
    console.log(r.clientOrderId, '→', r.data?.execution?.settlementStatus);
  }

  if (legA.status === 'rejected' || legB.status === 'rejected') {
    console.warn('at least one leg rejected', legA, legB);
  }
}

How to run this

  1. Set LIMITLESS_API_KEY and PRIVATE_KEY, this function fires two FOK legs. Wire it into the detection loop from Section 02 so it only runs when sellBoth > THRESH.
  2. Drop executeSellBoth into cross-market-arb.ts and call it from your orderbookUpdate handler.
  3. On a live arb, both legs fire in parallel, the /orders/status/batch call prints a settlementStatus for each clientOrderId, and a replay of the same UUID returns 409 Conflict, proof your idempotency key works.

Section 04

When arb isn’t risk-free.

Four risks every arb bot accepts

“Risk-free arbitrage” is a phrase people use right up until they learn otherwise. Model each of these explicitly before you turn size up.

Execution risk

FOK makes each leg atomic, but 429 rate limits and transient 5xxs eat the retry budget, by the time the second leg lands, the edge is gone. Wrap both submits in withRetry / retry_on_errors / WithRetry, and keep retry counts low.

Settlement risk

Two venues resolve the same event differently. Your “matched” arb ends up with one winning leg and one losing leg by decree.

Capital lockup

Collateral is tied up until resolution. Your 1% guaranteed edge over 90 days is a 4% APR, and you can’t deploy that capital anywhere else meanwhile.

Tax treatment

A matched book is one bet to the exchange. It is two bets to the tax authority. The winning leg is income, the losing leg may or may not be deductible. Talk to an accountant.

Common questions

Prediction market arbitrage: what people ask

Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.

  1. What is YES + NO parity arbitrage?
    On a binary market the YES and NO contracts must sum to $1. When the best bids sum above $1, sell both sides and pocket the difference; when the best asks sum below $1, buy both for less than the guaranteed $1 payout. Two legs, one venue, minimal risk, the cleanest arb class on a prediction market. Cross-platform and correlated-market arbs exist too, but they carry bridge, settlement-mismatch, and model-assumption risk respectively.
  2. How do you detect arbitrage in real time on Limitless?
    Subscribe to orderbookUpdate over the WebSocketClient (wss://ws.limitless.exchange) for a watch-list of slugs and recompute bid(YES) + bid(NO) and ask(YES) + ask(NO) on every delta. Flag only gaps that clear 1 + 2×FEE + BUFFER: fees for both legs plus a 30 bps safety margin. Two operational details: subscriptions are replaced on each call, so pass every slug in one go, and the server doesn’t persist subscriptions, so resubscribe on every reconnect.
  3. Why use FOK orders for arbitrage execution?
    Fill-or-kill makes each leg atomic: the order fills its full makerAmount or rejects outright, so there’s no half-filled state to roll back. Give each leg a unique clientOrderId; a duplicate submission returns 409 Conflict, which is safe to treat as already-submitted. Confirm both legs afterward with POST /orders/status/batch (1–50 items) and read back each leg’s settlementStatus.
  4. Is prediction market arbitrage actually risk-free?
    No. Four risks survive even a perfect detector: execution risk (429s and transient 5xxs delay the second leg until the edge is gone, so wrap submits in the SDK retry helper with low retry counts); settlement risk (two venues can resolve the same event differently, leaving one losing leg by decree); capital lockup (collateral is tied up until resolution, so a 1% edge over 90 days is a 4% APR); and tax treatment (the winning leg is income, the losing leg may not be deductible).
  5. What do you do when one arbitrage leg doesn’t fill?
    Hedge immediately. After each leg fills, check the next leg’s book against your fill price; if the projected edge has gone negative, take the small loss at the new market price instead of holding open exposure. A missed leg silently converts your arb into a directional position you never intended, and hoping the price comes back is a directional bet, not an arb.

Section 05

Module checklist.

Tick each item once you’ve actually done it. The Continue button unlocks at 5/5.

Module 16 complete

Free lunches spotted.

Your bot trades the cleanest edge in finance: prices that have to be wrong. When YES + NO doesn’t add to a dollar or sibling markets drift apart, your bot is the one closing the gap and pocketing the difference, in seconds, while you’re doing something else.

Concretely, your bot finds and executes parity arbs without you watching the book. Three things you walk away with:

01

A streaming detector over WebSocketClient that watches a slug list, computes bid(YES) + bid(NO) and ask(YES) + ask(NO) on every book delta, and only flags edges that clear a fee + 30 bps buffer.

02

A two-leg OrderType.FOK executor keyed by a shared clientOrderId UUID, plus a POST /orders/status/batch confirmation that reads back settlementStatus for both legs.

03

An honest mental model of the four risks an arb bot actually carries, execution, settlement, capital lockup, and tax treatment, so you can size with your eyes open.

Next up: trading on signals, not chart shapes, EMA crosses, probability divergences, and the feature/threshold/cooldown pattern every signal bot shares.

Complete the checklist above to unlock