Getting started

Quickstart

A sandbox key to a settled trade. Every call below is the real partner contract: run them against the host issued to you and you get the responses shown.

The one thing to get right

  1. 1Key
  2. 2Events
  3. 3Quote
  4. 4Reserve
  5. 5Order
  6. 6Position
  7. 7Exit
  8. 8Settle
  9. 9Reconcile

You own your customers’ cash ledger

Predicta never holds a player's funds. Every money-moving response carries the exact amount to apply against your own ledger: reserve authorizationAmount, debit actualDebit, release releaseAmount, credit sellProceeds on an exit and settlementCredit on a resolution.

Apply those figures. Never recompute one from a price and a quantity: the moment you multiply two floats there are two answers to one question, and only one of them is in our ledger.

Amounts inside those objects are exact decimal strings. The numbers beside them — stake, notional, predictaFee, averagePrice — are for display.

1. Get a sandbox key

A sandbox operator key is issued to you and shown exactly once. Sandbox keys are minted pk_test_ and live keys sk_live_; the spaces do not overlap, and the mode lives on your tenant rather than on anything you send.

shell
export PREDICTA_BASE="https://<issued-to-you>"
export PREDICTA_KEY="pk_test_..."      # operator credential, shown once
export PREDICTA_USER="player-8421"     # YOUR id for this player
typescript
import { PredictaClient } from '@/sdk';

const predicta = new PredictaClient({
  baseUrl: process.env.PREDICTA_BASE!,
  apiKey:  process.env.PREDICTA_KEY!,
});

const player = predicta.forUser(process.env.PREDICTA_USER!);
  • The key identifies the operator. X-Predicta-User identifies the end user and is required on every quote and order. There is no session and no login call.
  • A player exists the first time you name one on a write. Reads never create one, so a typo'd id surfaces as 404 unknown_user rather than a confident zero.

2. List events

GET /api/v1/events is the partner catalogue: one row per question, each carrying its outcomes. The id you quote against is the id of an outcome — an opaque ctr_… contract id — not of the event.

shell
curl -s "$PREDICTA_BASE/api/v1/events?limit=1&tradable=true" \
  -H "Authorization: Bearer $PREDICTA_KEY"
200 application/json (abridged)
{
  "data": [
    {
      "id": "evt_...",
      "slug": "fed-decision-in-september",
      "title": "Fed Decision in September?",
      "category": "economy",
      "kind": "mutually_exclusive",
      "imageUrl": "/api/v1/images/evt_...",
      "outcomes": [
        {
          "id": "ctr_9f2b...",
          "label": "No change",
          "probability": 0.715,
          "yesCents": 71.5,
          "executableYesCents": 72,
          "executableNoCents": 29,
          "spreadCents": 1,
          "tradable": true,
          "closesAt": "2026-09-16T00:00:00.000Z"
        }
      ]
    }
  ],
  "pagination": { "limit": 1, "total": 3751, "nextCursor": "…", "hasMore": true }
}

yesCents is a probability. executableYesCents is a price.

yesCents is the market's reference — usually a midpoint nobody trades at. A buy takes the ask and a sell takes the bid. Across 22,961 open contracts the ask sits a median 1.5¢ above the reference and 24.8% differ by more than 10¢, so a ticket rendering yesCents shows a number the player will not be charged.

When executableYesCents is null the book publishes no usable side: show “no price” and never fall back to the reference.

typescript
const page = await predicta.listEvents({ tradable: true, limit: 1 });
const outcome = page.items
  .flatMap((e) => e.outcomes)
  .find((o) => o.executableYesCents !== null)!;

3. Quote $25

A quote binds contract, side, price, fee and quantity together and holds them for 30 seconds. It is what the order is checked against, which is what makes “the screen said 72¢” answerable.

shell
curl -s -X POST "$PREDICTA_BASE/api/v1/quotes" \
  -H "Authorization: Bearer $PREDICTA_KEY" \
  -H "X-Predicta-User: $PREDICTA_USER" \
  -H "content-type: application/json" \
  -d '{"marketId":"ctr_9f2b...","side":"YES","stake":25}'
200 application/json
{
  "quoteId": "fc3be731-98dc-4c03-80ac-23576a708d9c",
  "contractId": "ctr_9f2b...",
  "side": "YES",
  "action": "buy",
  "executionPrice": 0.72,
  "priceBasis": "book",
  "stake": 25,
  "predictaFee": 0.3,
  "predictaFeeBps": 120,
  "tradeAmount": 24.7,
  "contracts": 34.3056,
  "potentialPayout": 34.31,
  "expiresAt": "2026-08-21T04:44:02.120Z",
  "simulated": true,
  "freshness": { "level": "fresh", "ageSeconds": 2, "feed": "websocket" },
  "routing": { "consideredVenues": 1, "routable": false },
  "operatorMoney": {
    "currency": "USD",
    "authorizationAmount": "25.00",
    "actualDebit": "25.00",
    "releaseAmount": "0.00",
    "tradeAmount": "24.70",
    "platformFee": "0.30",
    "venueFee": "0.00",
    "venueFeeKnown": false
  },
  "operatorExitMoney": null
}

The fee comes off the stake first ($25.00 − $0.30 = $24.70), the remainder buys contracts at executionPrice ($24.70 ÷ 0.72 = 34.31 contracts), and each contract settles at exactly $1.00 if the side is right. Every one of those figures is on the response — derive nothing.

typescript
const quote = await player.quoteBuy({ contractId: outcome.id, side: 'YES', stake: 25 });
// Show the player quote.executionPrice, not outcome.probability.

4. Reserve $25 in your own ledger

Before the order, not after. authorizationAmount is the gross cash the player commits — the fee is already inside it — so it is exactly what you hold.

typescript
const reservation = await wallet.reserve(quote.operatorMoney!.authorizationAmount);
// "25.00" — an exact decimal string. Parse it as a decimal, never as a float.

Derive the clientOrderId from the reservation

Not from a clock and not from a random value. clientOrderId is the idempotency key, and its whole purpose is to survive the crash that loses you our response — which means it has to be reconstructible from something you already wrote down.

5. Submit the order

shell
curl -s -X POST "$PREDICTA_BASE/api/v1/orders" \
  -H "Authorization: Bearer $PREDICTA_KEY" \
  -H "X-Predicta-User: $PREDICTA_USER" \
  -H "content-type: application/json" \
  -d '{"quoteId":"fc3be731-...","clientOrderId":"res-9031"}'
201 application/json
{
  "orderId": "3dd27f36-7780-4aa7-8af2-1a6854fbf8cb",
  "clientOrderId": "res-9031",
  "status": "filled",
  "contractId": "ctr_9f2b...",
  "side": "YES",
  "action": "buy",
  "filledQuantity": 34.3056,
  "averagePrice": 0.72,
  "notional": 24.7,
  "stake": 25,
  "rejectReason": null,
  "simulated": true,
  "idempotentReplay": false,
  "routing": { "reason": "only_venue", "consideredVenues": 1 },
  "operatorMoney": {
    "authorizationAmount": "25.00",
    "actualDebit": "25.00",
    "releaseAmount": "0.00",
    "tradeAmount": "24.70",
    "platformFee": "0.30",
    "venueFee": "0.00",
    "venueFeeKnown": false,
    "currency": "USD"
  },
  "operatorExitMoney": null
}

Apply the exact amounts

typescript
const order = await player.submitOrder({
  quoteId: quote.quoteId,
  clientOrderId: reservation.id,
});

await wallet.debit(order.operatorMoney!.actualDebit);      // "25.00"
await wallet.release(order.operatorMoney!.releaseAmount);  // "0.00" here
  • On a partial fill the release is non-zero. A venue that took $12.30 of a $25.00 authorization leaves $12.70 to hand back, and an operator that debits the full stake instead is keeping money that is not theirs. Both identities hold exactly: authorizationAmount = actualDebit + releaseAmount and actualDebit = tradeAmount + platformFee + venueFee.
  • 201 created this order. 200 with idempotentReplay: true replayed one you already had — retrying with the same clientOrderId is always safe and never opens a second position.
  • 409 quote_expired or 409 price_stale: re-quote and let the player confirm the new number. Never resend the same body, and never silently re-price on their behalf.

409 reconciliation_required is not a rejection

It means the venue was asked and has not answered. The order is at the venue or it is not, and Predicta does not yet know which — so collateral stays committed and you keep holding the authorization.

Releasing there is the most expensive mistake available on this API: it leaves a real position with no cash behind it, and you cannot discover the error because we told you it failed. Poll GET /api/v1/orders?externalUserId=&clientOrderId= — the id you chose — and act when settlementState reads settled.

6. Read the position

shell
curl -s "$PREDICTA_BASE/api/v1/positions?status=open&externalUserId=$PREDICTA_USER" \
  -H "Authorization: Bearer $PREDICTA_KEY"
200 application/json
{
  "positions": [
    {
      "positionId": "pos_...",
      "externalUserId": "player-8421",
      "contractId": "ctr_9f2b...",
      "side": "YES",
      "status": "open",
      "quantity": 34.3056,
      "averagePrice": 0.72,
      "openedAt": "2026-08-21T04:43:40.282Z",
      "updatedAt": "2026-08-21T04:43:40.282Z",
      "operatorMoney": { "currency": "USD", "costBasis": "24.70", "realisedPnl": "0.00" }
    }
  ],
  "count": 1,
  "nextCursor": null
}

operatorMoney here carries facts, not instructions: cash already spent on what is still held, and profit already booked on what was closed. Nothing on a position is an amount to move — those live on the order and on the settlement, and a position that also carried one would give you two places to book the same dollar from.

There is no mark-to-market, deliberately. A value derived from the current bid would change between two identical calls because a feed moved, and a number that moves on its own cannot be reconciled against. Want the exit value? Quote a sell — a real price, not an estimate.

7. Sell half of it

An exit is sized in contracts. A cash-sized sell cannot express “close this out”: inverting the fee rounding either oversells or strands a fraction.

shell
curl -s -X POST "$PREDICTA_BASE/api/v1/quotes" \
  -H "Authorization: Bearer $PREDICTA_KEY" \
  -H "X-Predicta-User: $PREDICTA_USER" \
  -H "content-type: application/json" \
  -d '{"marketId":"ctr_9f2b...","side":"YES","action":"sell","contracts":17.15}'
typescript
const exitQuote = await player.quoteSell({
  contractId: held.contractId,
  side: held.side,
  contracts: held.quantity / 2,
});

const exit = await player.submitOrder({
  quoteId: exitQuote.quoteId,
  clientOrderId: `exit-${reservation.id}`,
});

// A sell CREDITS. operatorMoney is null on an exit, deliberately.
await wallet.credit(exit.operatorExitMoney!.sellProceeds);
200 application/json (the money half)
{
  "action": "sell",
  "operatorMoney": null,
  "operatorExitMoney": {
    "currency": "USD",
    "sellProceeds": "12.20",
    "platformFee": "0.15",
    "venueFee": "0.00",
    "venueFeeKnown": false
  }
}

sellProceeds is already net

The fees beside it are reported so you can show the player what the round trip cost — not so you can subtract them. Subtracting again is a double charge, and operatorMoney is null here precisely so a wallet applying the entry contract by habit cannot debit a player for selling their own position.

Read the position again: quantity has shrunk and realisedPnl has moved. The remainder is still open.

8. Credit the settlement

When the contract resolves, Predicta writes one settlement per position with the amount to credit already computed. Two places computing one payout is how they come to disagree.

shell
curl -s "$PREDICTA_BASE/api/v1/settlements?limit=100" \
  -H "Authorization: Bearer $PREDICTA_KEY"
200 application/json
{
  "settlements": [
    {
      "id": "stl_...",
      "positionId": "pos_...",
      "externalUserId": "player-8421",
      "side": "YES",
      "resolution": "YES",
      "outcome": "won",
      "contracts": 17.15,
      "realisedPnl": 4.8,
      "lifetimeRealisedPnl": 5.9,
      "settledAt": "2026-08-22T18:02:11.004Z",
      "operatorMoney": {
        "currency": "USD",
        "settlementCredit": "17.15",
        "voidCredit": "0.00"
      },
      "cursor": "…"
    }
  ],
  "nextCursor": null,
  "hasMore": false
}
typescript
for await (const s of predicta.iterateSettlements({ settledFrom: lastRun })) {
  if (s.outcome === 'won')  await wallet.credit(s.operatorMoney.settlementCredit, s.id);
  if (s.outcome === 'void') await wallet.refund(s.operatorMoney.voidCredit, s.id);
}
  • Exactly one of the two is non-zero; a loss zeroes both. They are separate fields because they are separate instructions to book — winnings against a wager, versus a refund that reverses one.
  • Key your credit on the settlement id. It is stable, which is what makes the consumer safe to run twice after a crash.
  • realisedPnl is this settlement's figure. lifetimeRealisedPnl is the position's running total, and the two differ whenever the position was partly exited — as yours just was.

9. Reconcile

shell
curl -s "$PREDICTA_BASE/api/v1/reconciliation" \
  -H "Authorization: Bearer $PREDICTA_KEY"
typescript
const report = await predicta.reconciliationReport();
if (!report.ok) escalate(report.findings);

// And the fact stream your own books are rebuilt from:
for await (const fact of predicta.iterateFeed({ cursor: storedCursor })) {
  await apply(fact);          // idempotent on fact.id
  storedCursor = fact.cursor; // advance only once applied
}

Reading the feed does not consume it: the same cursor returns the same items forever, so a consumer that dies mid-batch simply re-reads. The full contract is on Reconciliation.

You are integrated when

  • Your ticket shows the quote's executionPrice, never the reference.
  • You reserve authorizationAmount, debit actualDebit and release releaseAmount — and none of those three is ever recomputed on your side.
  • An exit credits sellProceeds without subtracting the fees again.
  • A 409 re-quotes; a 409 reconciliation_required or a timeout holds and polls.
  • Your order retries reuse the same clientOrderId.
  • Your settlement consumer keys on the settlement id and can run twice safely.
  • node scripts/conformance.mjs --base $PREDICTA_BASE --key $PREDICTA_KEY passes against your own key.