Predik Agent API

Build autonomous trading agents that interact with Predik prediction markets. Register your agent wallet via EIP-712 signatures, receive API keys, and trade on real-world events with transparent on-chain odds.

Chain
Base (Coinbase L2)
Chain ID: 84532
Settlement
Yellow Network
Gasless state-channel trading
Auth
EIP-712 + API Keys
One-time registration, key rotation

Quickstart

Get your agent trading in under 5 minutes. This example uses viem on Base (chain ID 8453).

import crypto from 'node:crypto';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';

const BASE_URL = 'https://app.predik.io/api/v2';
const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY');

// 1. Get a nonce
const { nonce } = await fetch(
  `${BASE_URL}/auth/nonce?wallet=${account.address}`
).then(r => r.json());

// 2. Sign EIP-712 message
const timestamp = Math.floor(Date.now() / 1000);
const signature = await account.signTypedData({
  domain: {
    name: 'Predik Agent API',
    version: '1',
    chainId: 84532, // Base Sepolia
    verifyingContract: '0x0000000000000000000000000000000000000000',
  },
  types: {
    RegisterAgent: [
      { name: 'wallet', type: 'address' },
      { name: 'nonce', type: 'string' },
      { name: 'timestamp', type: 'uint256' },
      { name: 'action', type: 'string' },
    ],
  },
  primaryType: 'RegisterAgent',
  message: {
    wallet: account.address,
    nonce,
    timestamp: BigInt(timestamp),
    action: 'register',
  },
});

// 3. Register and get API key
const { apiKey, signerStatus } = await fetch(`${BASE_URL}/auth/register-agent`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    wallet: account.address,
    signature,
    nonce,
    timestamp,
  }),
}).then(r => r.json());

// 3b. Wait for signer to become active (may be "pending" after registration)
async function waitForSigner(key: string, maxAttempts = 10) {
  for (let i = 0; i < maxAttempts; i++) {
    const res = await fetch(`${BASE_URL}/auth/retry-signer`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${key}` },
    }).then(r => r.json());
    if (res.signerStatus === 'active') return;
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
  throw new Error('Signer activation timed out');
}

if (signerStatus !== 'active') {
  console.log('Signer pending, waiting for activation...');
  await waitForSigner(apiKey);
}

// 4. Claim USDC from faucet
await fetch(`${BASE_URL}/yellow/faucet/claim`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ amount: '10.00' }),
});

// 5. Wait for a fresh projected balance if needed
await fetch(`${BASE_URL}/yellow/unified-balance?waitForSync=true&timeout=10`, {
  headers: {
    Authorization: `Bearer ${apiKey}`,
  },
});

// 6. Get a live quote
const marketId = 'YOUR_MARKET_ID';
const quote = await fetch(
  `${BASE_URL}/markets/${marketId}/quote?side=BUY&outcomeIndex=0&amount=10000000`
).then(r => r.json());

// 7. Lock the quote for 15 seconds
const { quoteId } = await fetch(`${BASE_URL}/markets/${marketId}/quote-lock`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    side: 'BUY',
    outcomeIndex: 0,
    amount: '10.00',
  }),
}).then(r => r.json());

// 8. Refresh the session before live trading
await fetch(`${BASE_URL}/markets/${marketId}/session?refresh=1`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
  },
});
await new Promise(resolve => setTimeout(resolve, 1500));

// 9. Execute trade
const trade = await fetch(`${BASE_URL}/markets/${marketId}/trade`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
    'x-idempotency-key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    side: 'BUY',
    outcomeIndex: 0,
    amount: '10.00',
    quoteId,
  }),
}).then(r => r.json());

console.log('Trade executed:', trade);
Step 1-3
Register once per wallet. Store the API key securely -- it is shown exactly once.
Step 4-6
Claim USDC from faucet, check quotes, and execute trades. Always include an idempotency key.

Agent Guide#

Everything an autonomous agent needs to know before calling the Predik API.


1. Market Lifecycle & Action Map#

Every market progresses through a fixed lifecycle. Different API endpoints become available at each stage.

Draft → PreMarket → [graduation] → Live → [resolution] → Resolved → Settled
                                                          ↘ Disputed → Resolved

If graduation thresholds are not met before the deadline, the market is voided and all commitments are refunded automatically.

Action Map#

Market StateAvailable ActionsKey Endpoints
PreMarketQuote a commitment, Commit USDCPOST /pricing/pre-market/quote, POST /commitments
LiveQuote a trade, Buy/Sell sharesGET /markets/{id}/quote, POST /markets/{id}/trade
Resolved / SettlingWait for settlement, then claim payoutmarket.claimable webhook or GET /markets/{id} (poll settlementStatus), GET /settlement/claim, POST /settlement/claim
AnyBrowse markets, View positions, Check oddsGET /markets, GET /markets/{id}, GET /markets/{id}/positions, GET /markets/{id}/odds-history

Graduation Thresholds#

A PreMarket graduates to Live when both conditions are met before the deadline:

  • Minimum liquidity: $100+ USDC total committed
  • Minimum committers: 10+ unique wallets
  • Anti-whale cap: No single user may exceed 33% of target liquidity

Resolution Flow#

  1. Market locks — trading stops
  2. UMA Optimistic Oracle receives a price request
  3. A proposer posts the outcome with a 500 USDC bond
  4. 4-hour liveness period for disputes
  5. If undisputed → outcome is final; if disputed → UMA DVM token-holder vote (~48h)
  6. Resolution paths trigger backend settlement immediately; loser sessions are closed first, then winner sessions are funded and closed
  7. When settlement completes, agents subscribed to market.claimable receive a webhook; otherwise poll GET /markets/{id} until settlementStatus === "completed"
  8. Winners call POST /settlement/claim only after settlementStatus === "completed"

Important: Claims will return 409 SETTLEMENT_PENDING if called before the backend settlement completes. Prefer the market.claimable webhook when registered; otherwise poll GET /markets/{id} and wait for settlementStatus === "completed" before attempting a claim.

Voided markets → full refund at cost basis, no claim action needed.


2. Authentication & Signer Readiness#

Official SDKs are available in-repo:

  • TypeScript: packages/agent-sdk
  • Python preview: sdks/python

API Key Format#

All authenticated endpoints require a Bearer token in the format:

Authorization: Bearer pk_live_...

Keys are 51 characters, issued once per wallet via POST /auth/register-agent. Store securely — the key is shown exactly once.

Signer Provisioning#

When you register, the response includes signerStatus. If it is "pending", you must poll until it becomes "active" before calling any mutation endpoint (faucet, trades, commitments).

// Poll until signer is active
async function waitForSigner(apiKey: string, maxAttempts = 10) {
  for (let i = 0; i < maxAttempts; i++) {
    const res = await fetch(`${BASE_URL}/auth/retry-signer`, {
      method: "POST",
      headers: { Authorization: `Bearer ${apiKey}` },
    }).then((r) => r.json());
    if (res.signerStatus === "active") return;
    await new Promise((r) => setTimeout(r, 2000));
  }
  throw new Error("Signer activation timed out");
}

If you call a mutation while the signer is pending, you will receive:

{
  "code": "SIGNER_NOT_READY",
  "error": "SIGNER_NOT_READY",
  "message": "Delegated signer not ready. Call /api/v2/auth/retry-signer first.",
  "retryable": true,
  "retryAfterMs": 30000
}

Key Lifecycle#

ActionEndpointNotes
RegisterPOST /auth/register-agentOne key per wallet, returns pk_live_...
RotatePOST /auth/rotate-keyInvalidates old key, returns new one
RevokeDELETE /auth/revoke-keyPermanently invalidates key
ListGET /auth/keysShows active keys for your wallet

3. Amount Units — CRITICAL#

Different endpoints use different units. Mixing them up is the most common agent bug.

ContextFormatExampleUsed By
Trade body amountDecimal string (USDC)"10.00"POST /markets/{id}/trade
Commitment body amountDecimal string (USDC)"10.00"POST /commitments
Faucet body amountDecimal string (USDC)"10.00"POST /yellow/faucet/claim
Pre-market quote body amountDecimal string (USDC)"10.00"POST /pricing/pre-market/quote
Live quote query amountMicros integer string"10000000"GET /markets/{id}/quote
Balance/faucet response amountsMicros integer10000000Response fields
Commitment response amountsDecimal string (USDC)"25.50"POST /commitments response
Commitments list amountsJS number (float)25.0GET /markets/{id}/commitments
Position quantitiesDecimal string (USDC)"150.50"GET /markets/{id}/positions
Claim eligibility positionMicros integer string"15000000"GET /settlement/claim
Claim eligibility payoutMicros integer string"15000000"GET /settlement/claim
Claim response claim.payoutDecimal USDC string"25"POST /settlement/claim

Conversion#

micros = parseFloat(decimal) * 1_000_000
decimal = (micros / 1_000_000).toFixed(6)

Example: To trade $10.00 USDC:

  • In POST /markets/{id}/trade body → "amount": "10.00" (decimal string)
  • In GET /markets/{id}/quote query → ?amount=10000000 (micros)

Rule of Thumb#

  • Request bodies → decimal strings ("10.00")
  • Query parameters → micros integer strings ("10000000")
  • Response values → format varies by endpoint (see table above). Balance and faucet return micros; commitment and position endpoints return decimals or floats

4. Testnet & Faucet Semantics#

Faucet#

The faucet dispenses ytest.usd — the canonical Yellow Network testnet asset:

curl -X POST https://app.predik.io/api/v2/yellow/faucet/claim \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"amount": "10.00"}'
ParameterValue
Default claim10 ytest.usd
Maximum per claim100 ytest.usd
Rate limit10 claims / minute / wallet
Response amountMicros integer (e.g., 10000000 = 10.00 USDC)

Unified Balance#

After claiming, funds appear in your Unified Balance — a single credit across all Yellow Network sessions. Check it with:

GET /yellow/unified-balance
Authorization: Bearer pk_live_...

The amount in the response is in micros.

Use GET /yellow/unified-balance?waitForSync=true&timeout=10 after faucet claims if you need the route to block until projected or relay balance data is fresh.

Note for API-key agents: When relay reads are unavailable, the endpoint returns wallet-scoped projected balances with source: "db_projection". This keeps available balances accurate after faucet claims and pre-market commitments.

Fund Destination#

When you claim from the faucet, funds are credited to the delegated signer address used for Yellow mutations. The unified balance endpoint still reports the wallet-scoped available balance projection, so your agent should not need to track the delegated signer address itself.

4.1 Market Object Semantics#

  • Prefer questionEs / questionEn when present, otherwise fall back to question
  • Use resolutionTime for expiry / resolution filtering
  • Live market odds are on the top-level market object:
    • liveOdds: current CS-LMSR odds keyed by stringified outcome index
    • sentimentOdds: pre-market commitment-weighted odds keyed by stringified outcome index
  • Outcome objects only contain id and label; do not expect odds inside outcomes[]

5. Strategy Rules#

Picking Markets#

  • Filter by state: GET /markets?state=PreMarket or GET /markets?state=Live
  • Check odds history: GET /markets/{id}/odds-history?timeframe=7d
  • Review commitment activity: GET /markets/{id}/commitments
  • Look up markets by slug: GET /markets/by-slug/{slug}

Pre-Market Strategy#

  • Anti-whale cap: Max 33% of target liquidity per user — amounts above this are rejected
  • Earlier commitments get better prices (bonding curve: price rises with total committed)
  • Commitments are locked until graduation or voiding (no early exit)
  • If the market voids, you get a full refund at cost basis

Pre-Market Commitment Request#

curl -X POST https://app.predik.io/api/v2/commitments \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "marketId": "uuid-here",
    "outcomeId": 0,
    "amount": "10.00"
  }'
FieldTypeDescription
marketIdstringMarket UUID (required)
outcomeIdintegerOutcome index: 0 or 1 (required, NOT outcomeIndex)
amountstringDecimal USDC amount (required, e.g., "10.00")

Trade Execution#

  • Minimum trade: $1 USDC
  • Fee: 1% on buys only (sells are free)
  • Always set x-idempotency-key on trades (required, full replay semantics). For commitments the header is optional and provides best-effort duplicate prevention only — see §Idempotency below
  • Before live trades, refresh the session with POST /markets/{id}/session?refresh=1, wait 1-2 seconds, then call POST /markets/{id}/trade
  • Use POST /markets/{id}/quote-lock when you need a short-lived quoteId to carry into the trade request
  • No shorting — to bet against an outcome, buy the opposite outcome

Retry Patterns#

HTTP StatusCodeAction
429RATE_LIMIT_EXCEEDEDRead Retry-After header, wait, then retry with exponential backoff + jitter
503SIGNER_NOT_READYPoll POST /auth/retry-signer every 2s until signerStatus === "active"
409 / 503SESSION_DESYNCRespect retryAfterMs; if needed call POST /markets/{id}/session?refresh=1, wait 1-2s, then retry
409SETTLEMENT_PENDINGPoll GET /markets/{id} until settlementStatus === "completed", then retry the claim
409IDEMPOTENCY_IN_PROGRESSWait retryAfterMs, then retry with the same idempotency key (do NOT generate a new one)

Idempotency#

Trades (POST /markets/{id}/trade) — the x-idempotency-key header is required. Full replay semantics:

  • Keys are hashed via SHA-256 from the canonical request JSON
  • TTL: 24 hours
  • Stale pending threshold: 5 minutes (after which a stuck key can be reused)
  • Replaying an identical request returns the original result with x-idempotent-replay: true
  • Always reuse the same key when retrying a failed trade request

Commitments (POST /commitments) — the x-idempotency-key header is optional but recommended. When provided, full replay semantics (identical to trades):

  • Keys are hashed via SHA-256 from the canonical request JSON (marketId, userAddress, outcomeId, amount)
  • TTL: 24 hours
  • Stale pending threshold: 5 minutes
  • Replaying an identical request returns the original result with x-idempotent-replay: true
  • Payload mismatch (same key, different body) returns 422 IDEMPOTENCY_PAYLOAD_MISMATCH
  • Retryable failures (5xx, 429) allow retry with the same key immediately
  • Omitting the header provides no duplicate protection at all

Agent Authentication Walkthrough#

This guide walks you through registering an autonomous trading agent on Predik and making your first trade. Total time: ~5 minutes.


Prerequisites#

  • An Ethereum wallet with a private key (Base Sepolia testnet, chainId 84532)
  • The AGENT_API_ENABLED feature flag must be true on the server
  • USDC deposited in your Yellow Network custody balance

Step 1: Request a Nonce#

WALLET="0xYourWalletAddress"

curl -s "https://app.predik.io/api/v2/auth/nonce?wallet=$WALLET" | jq

Response:

{
  "nonce": "a1b2c3d4e5f6...64_hex_chars",
  "expiresIn": 300
}

Save the nonce value. It expires in 5 minutes and can only be used once.


Step 2: Sign the EIP-712 Message#

The signature proves you own the wallet. Sign the following EIP-712 typed data:

Domain:

name: "Predik Agent API"
version: "1"
chainId: 84532
verifyingContract: 0x0000000000000000000000000000000000000000

Message (RegisterAgent type):

wallet: <your checksummed address>
nonce: <nonce from step 1>
timestamp: <current unix timestamp in seconds>
action: "register"

TypeScript (viem)#

import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount("0xYourPrivateKey");
const timestamp = Math.floor(Date.now() / 1000);

const signature = await account.signTypedData({
  domain: {
    name: "Predik Agent API",
    version: "1",
    chainId: 84532,
    verifyingContract: "0x0000000000000000000000000000000000000000",
  },
  types: {
    RegisterAgent: [
      { name: "wallet", type: "address" },
      { name: "nonce", type: "string" },
      { name: "timestamp", type: "uint256" },
      { name: "action", type: "string" },
    ],
  },
  primaryType: "RegisterAgent",
  message: {
    wallet: account.address,
    nonce: NONCE,
    timestamp: BigInt(timestamp),
    action: "register",
  },
});

Python (eth_account)#

from eth_account import Account
from eth_account.messages import encode_typed_data
import time

private_key = "0xYourPrivateKey"  # pragma: allowlist secret
account = Account.from_key(private_key)
timestamp = int(time.time())

typed_data = {
    "types": {
        "EIP712Domain": [
            {"name": "name", "type": "string"},
            {"name": "version", "type": "string"},
            {"name": "chainId", "type": "uint256"},
            {"name": "verifyingContract", "type": "address"},
        ],
        "RegisterAgent": [
            {"name": "wallet", "type": "address"},
            {"name": "nonce", "type": "string"},
            {"name": "timestamp", "type": "uint256"},
            {"name": "action", "type": "string"},
        ],
    },
    "primaryType": "RegisterAgent",
    "domain": {
        "name": "Predik Agent API",
        "version": "1",
        "chainId": 84532,
        "verifyingContract": "0x0000000000000000000000000000000000000000",
    },
    "message": {
        "wallet": account.address,
        "nonce": NONCE,
        "timestamp": timestamp,
        "action": "register",
    },
}

signed = account.sign_typed_data(typed_data["domain"], typed_data["types"], typed_data["message"])
signature = signed.signature.hex()

Foundry (cast)#

TIMESTAMP=$(date +%s)

cast wallet sign-typed-data \
  --private-key $PRIVATE_KEY \
  --domain "name:Predik Agent API,version:1,chainId:84532,verifyingContract:0x0000000000000000000000000000000000000000" \
  --type "RegisterAgent(address wallet,string nonce,uint256 timestamp,string action)" \
  --data "wallet:$WALLET,nonce:$NONCE,timestamp:$TIMESTAMP,action:register"

Step 3: Register#

curl -X POST "https://app.predik.io/api/v2/auth/register-agent" \
  -H "Content-Type: application/json" \
  -d "{
    \"wallet\": \"$WALLET\",
    \"signature\": \"$SIGNATURE\",
    \"nonce\": \"$NONCE\",
    \"timestamp\": $TIMESTAMP
  }"

Response:

{
  "apiKey": "pk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ01234567890123",
  "walletAddress": "0xyouraddress...",
  "signerStatus": "active",
  "message": "Agent registered successfully. Store this API key securely..."
}

Store the apiKey immediately. It is shown exactly once and cannot be retrieved again.

If signerStatus is "pending", your delegated signer is still being provisioned. You must wait for it to become "active" before making any authenticated mutation (faucet claims, trades, commitments). Poll POST /api/v2/auth/retry-signer until signerStatus returns "active":

async function waitForSigner(apiKey: string, maxAttempts = 10) {
  for (let i = 0; i < maxAttempts; i++) {
    const res = await fetch(`${BASE_URL}/auth/retry-signer`, {
      method: "POST",
      headers: { Authorization: `Bearer ${apiKey}` },
    }).then((r) => r.json());
    if (res.signerStatus === "active") return;
    await new Promise((r) => setTimeout(r, 2000)); // poll every 2s
  }
  throw new Error("Signer activation timed out");
}

Note: Calling any mutation endpoint while signerStatus is "pending" returns 503 SIGNER_NOT_READY.


Step 4: Make a Trade#

All trade requests require:

  • Authorization: Bearer pk_live_... header
  • x-idempotency-key header (unique per request, prevents duplicate trades)
  • Amounts as decimal strings (e.g. "10.00"), not micros
  • Side as "BUY" or "SELL"
API_KEY="pk_live_yourKeyHere..."  # pragma: allowlist secret
MARKET_ID="your-market-id"

curl -X POST "https://app.predik.io/api/v2/markets/$MARKET_ID/trade" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "x-idempotency-key: $(uuidgen)" \
  -d '{"side": "BUY", "outcomeIndex": 0, "amount": "10.00"}'

Key Lifecycle#

Rotate Key#

Replace your current key with a new one (old key is immediately revoked):

curl -X POST "https://app.predik.io/api/v2/auth/rotate-key" \
  -H "Authorization: Bearer $API_KEY"

Revoke Key#

Permanently deactivate your key (irreversible):

curl -X DELETE "https://app.predik.io/api/v2/auth/revoke-key" \
  -H "Authorization: Bearer $API_KEY"

List Keys#

View all keys (active and revoked) for your wallet:

curl "https://app.predik.io/api/v2/auth/keys" \
  -H "Authorization: Bearer $API_KEY"

Important Notes#

  1. One active key per wallet. To get a new key, either rotate or revoke+re-register.
  2. Idempotency keys are required for all trade requests made with API key auth. Use UUIDs.
  3. Amounts are decimal strings, not integer micros. "10.00" means 10 USDC.
  4. The side parameter uses "BUY" or "SELL" (not "action").
  5. Signer status must be "active" before any authenticated mutation (faucet, trades, commitments). If "pending", poll /auth/retry-signer until active. Mutations return 503 SIGNER_NOT_READY while pending.
  6. Rate limits apply per-IP for public endpoints and per-wallet for authenticated endpoints. See rate-limits.md.

Agent Trading API Reference#

Base URL: https://app.predik.io/api/v2 > Feature flag: AGENT_API_ENABLED must be true on the server.

All agent endpoints require the feature flag. When disabled, agent-specific routes and API-key authenticated paths return 403 AGENT_API_DISABLED.


Authentication#

Two auth methods are supported via the Authorization: Bearer <token> header:

MethodToken FormatUse Case
API Keypk_live_... (51 chars)Autonomous agents
JWTDynamic-issued JWTBrowser users

Both methods produce the same internal identity (userId, walletAddress). All existing endpoints accept either.


Agent Registration Flow#

1. GET /auth/nonce#

Request a one-time nonce for EIP-712 signing.

Auth: None (public, rate-limited by IP)

GET /api/v2/auth/nonce?wallet=0xYourWalletAddress

Query Parameters:

ParamTypeRequiredDescription
walletstringYesEthereum address (checksummed or lowercase)

Success Response (200):

{
  "nonce": "a1b2c3d4...64_hex_chars",
  "expiresIn": 300
}

Errors:

StatusCodeCause
400INVALID_WALLET_ADDRESSInvalid wallet address
403AGENT_API_DISABLEDFeature flag disabled
429RATE_LIMIT_EXCEEDED>10 requests/min from this IP

curl:

curl "https://app.predik.io/api/v2/auth/nonce?wallet=0xYourAddress"

2. POST /auth/register-agent#

Register a new agent and receive an API key. The nonce from step 1 must be signed with EIP-712.

Auth: None (nonce + signature prove identity)

POST /api/v2/auth/register-agent
Content-Type: application/json

Request Body:

FieldTypeRequiredDescription
walletstringYesEthereum address (must match nonce request)
signaturestringYesEIP-712 signature (0x...)
noncestringYesNonce from step 1
timestampnumberYesUnix timestamp (seconds), must be within 300s of server time

Success Response (201):

{
  "apiKey": "pk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345678901234",
  "walletAddress": "0xyouraddress...",
  "signerStatus": "active",
  "message": "Agent registered successfully. Store this API key securely -- it cannot be retrieved again."
}

Errors:

StatusCodeCause
400VALIDATION_ERRORInvalid request body
401INVALID_NONCENonce expired, consumed, or wallet mismatch
401INVALID_SIGNATUREEIP-712 signature verification failed
409KEY_ALREADY_EXISTSActive key exists; revoke it first or use rotate-key
429RATE_LIMIT_EXCEEDED>5 requests/min from this IP

curl:

curl -X POST "https://app.predik.io/api/v2/auth/register-agent" \
  -H "Content-Type: application/json" \
  -d '{
    "wallet": "0xYourAddress",
    "signature": "0xSignedData...",
    "nonce": "a1b2c3d4...",
    "timestamp": 1709712000
  }'

Key Management#

3. POST /auth/rotate-key#

Atomically revoke the current key and issue a new one.

Auth: Bearer pk_live_... (current key)

POST /api/v2/auth/rotate-key
Authorization: Bearer pk_live_currentKey...

Success Response (200):

{
  "apiKey": "pk_live_newKeyHere...",
  "walletAddress": "0xyouraddress...",
  "previousKeyRevoked": true
}

Errors:

StatusCodeCause
401INVALID_API_KEYKey not found
401KEY_REVOKEDCurrent key already revoked
404NO_ACTIVE_KEYNo active key for this wallet

4. DELETE /auth/revoke-key#

Permanently revoke the current API key. Irreversible.

Auth: Bearer pk_live_...

DELETE /api/v2/auth/revoke-key
Authorization: Bearer pk_live_yourKey...

Success Response (200):

{
  "revoked": true,
  "walletAddress": "0xyouraddress...",
  "message": "API key has been permanently revoked."
}

5. GET /auth/keys#

List all API keys (active and revoked) for the authenticated wallet.

Auth: Bearer pk_live_... or JWT

GET /api/v2/auth/keys
Authorization: Bearer pk_live_yourKey...

Success Response (200):

{
  "keys": [
    {
      "id": "uuid-...",
      "keyPrefix": "pk_live_aBcDeFgHiJkL",
      "signerStatus": "active",
      "createdAt": "2026-03-06T12:00:00.000Z",
      "lastUsedAt": "2026-03-06T13:00:00.000Z",
      "revokedAt": null,
      "isActive": true
    }
  ]
}

Note: keyHash is never returned. Keys are identified by their 20-character prefix.


6. POST /auth/retry-signer#

Retry delegated signer provisioning if it failed during registration.

Auth: Bearer pk_live_...

POST /api/v2/auth/retry-signer
Authorization: Bearer pk_live_yourKey...

Success Response (200):

{
  "signerStatus": "active",
  "message": "Delegated signer is now active."
}

Errors:

StatusCodeCause
503SIGNER_PROVISIONING_FAILEDProvisioning timed out; retry in 30s

Trading#

7. POST /markets/{id}/trade#

Execute a trade on a live market.

Auth: Bearer pk_live_... or JWT

POST /api/v2/markets/{id}/trade
Authorization: Bearer pk_live_yourKey...
Content-Type: application/json
x-idempotency-key: unique-request-id

Headers:

HeaderRequiredDescription
AuthorizationYesAPI key or JWT
Content-TypeYesapplication/json
x-idempotency-keyYes (agents)Unique per-request ID to prevent duplicate trades

Request Body:

FieldTypeRequiredDescription
side"BUY" or "SELL"YesTrade direction
outcomeIndexnumberYesWhich outcome to trade (0 or 1)
amountstringYesUSDC amount as a decimal string (e.g. "10.00")
maxCoststringNoBUY-side slippage cap
minSharesstringNoBUY-side minimum share output
minPayoutstringNoSELL-side minimum payout
sessionIdstring | nullNoOptional Yellow session override
quoteIdstring | nullNoQuote lock ID from POST /markets/{id}/quote-lock

Important: For live trading, call POST /markets/{id}/session?refresh=1, wait 1-2 seconds, then submit the trade. If you need a bounded execution window, create a quote lock first and include quoteId.

Success Response (200):

{
  "success": true,
  "marketId": "market-uuid",
  "tradeId": "trade-uuid",
  "side": "BUY",
  "outcomeIndex": 0,
  "trade": {
    "sharesDelta": "100",
    "grossCost": "10.00",
    "fee": "0.10",
    "netCost": "10.10",
    "avgPrice": 0.1,
    "priceImpact": 0.001,
    "priceBefore": 0.5,
    "priceAfter": 0.501
  }
}

Errors:

StatusCodeCause
400IDEMPOTENCY_KEY_REQUIREDMissing x-idempotency-key header (agent auth only)
409IDEMPOTENCY_IN_PROGRESS / QUOTE_LOCK_EXPIRED / QUOTE_LOCK_MISMATCH / SESSION_DESYNCRequest conflict or recoverable session issue
422IDEMPOTENCY_PAYLOAD_MISMATCHSame idempotency key reused with a different trade payload
503SIGNER_NOT_READYDelegated signer not yet provisioned

curl:

curl -X POST "https://app.predik.io/api/v2/markets/MARKET_ID/trade" \
  -H "Authorization: Bearer pk_live_yourKey..." \
  -H "Content-Type: application/json" \
  -H "x-idempotency-key: $(uuidgen)" \
  -d '{"side": "BUY", "outcomeIndex": 0, "amount": "10.00", "quoteId": "QUOTE_LOCK_UUID"}'

Yellow Network#

8. POST /yellow/faucet/claim#

Claim testnet tokens from the Yellow Network faucet. The server checks its own balance, tops up from the protocol faucet if needed, then transfers tokens to the authenticated wallet's canonical Yellow participant (delegated signer for API-key agents) via a state channel.

Auth: Bearer pk_live_... or JWT

POST /api/v2/yellow/faucet/claim
Authorization: Bearer pk_live_yourKey...
Content-Type: application/json

Headers:

HeaderRequiredDescription
AuthorizationYesAPI key or JWT
Content-TypeYesapplication/json
x-active-wallet-addressNoOptional hint only. Authenticated wallet is authoritative if provided

Request Body:

FieldTypeRequiredDescription
amountstringNoUSDC amount as decimal string (e.g. "10.00"). Default "10.00", max "100.00"
assetstringNoAsset identifier. Overridden to canonical asset (ytest.usd) regardless of input

Success Response (200):

{
  "success": true,
  "asset": "ytest.usd",
  "amount": "10000000",
  "source": "protocol_faucet_server_transfer",
  "serverSigner": "0xServerAddress...",
  "destination": "0xYourAddress...",
  "transfer": {},
  "faucetTopUp": null
}

Note: amount in the response is in micros (1e6 scale). "10000000" = 10.00 USDC.

Errors:

StatusCodeCause
400VALIDATION_ERRORAmount <= 0 or > 100.00
429RATE_LIMIT_EXCEEDED>10 requests/min per wallet (sliding window)
500FAUCET_CLAIM_FAILEDServer transfer or faucet top-up failed
503FAUCET_LIQUIDITY_UNAVAILABLEInsufficient server balance after top-up attempt

curl:

curl -X POST "https://app.predik.io/api/v2/yellow/faucet/claim" \
  -H "Authorization: Bearer pk_live_yourKey..." \
  -H "Content-Type: application/json" \
  -d '{"amount": "10.00"}'

9. GET /yellow/unified-balance#

Fetch the authenticated user's unified balance across Yellow Network state channels. Returns on-chain balances and locked amounts (funds committed to open sessions). The endpoint attempts a live relay read and falls back to a database projection if the relay is unavailable.

Auth: Bearer pk_live_... or JWT

GET /api/v2/yellow/unified-balance?waitForSync=true&timeout=10
Authorization: Bearer pk_live_yourKey...

Headers:

HeaderRequiredDescription
AuthorizationYesAPI key or JWT
x-active-wallet-addressNoOptional hint only. Authenticated wallet is authoritative if provided

Success Response (200):

{
  "success": true,
  "userAddress": "0xYourAddress...",
  "balances": [{ "asset": "ytest.usd", "amount": "50000000" }],
  "lockedByAsset": {
    "ytest.usd": "10000000"
  },
  "source": "thin_relay",
  "stale": false,
  "lastFetchedAt": "2026-03-06T12:00:00.000Z"
}

Response Fields:

FieldTypeDescription
balancesArray<{asset, amount}>Current balances. amount is in micros (string). API-key agents may receive projected balances here
lockedByAssetRecord<string, string>Funds locked in open Yellow sessions, keyed by asset
source"thin_relay" or "db_projection"Whether data came from live relay or DB fallback
stalebooleantrue when the route could not return fresh relay or projected balance data
diagnosticobjectOptional fallback metadata when the route serves a wallet-scoped projection

Errors:

StatusCodeCause
409BALANCE_SYNC_TIMEOUTwaitForSync=true timed out before fresh relay or projection data was available
503UNIFIED_BALANCE_UNAVAILABLEFailed to compute balance snapshot

curl:

curl "https://app.predik.io/api/v2/yellow/unified-balance" \
  -H "Authorization: Bearer pk_live_yourKey..."

Markets#

10. GET /markets#

List V2 markets with filtering, sorting, and pagination. Results are cached at the edge (60s) and in Redis.

Auth: None (public, rate-limited by IP)

GET /api/v2/markets?state=Live&sort=volume&limit=20&offset=0

Query Parameters:

ParamTypeRequiredDefaultDescription
statestringNoComma-separated market states to filter. Values: Draft, PreMarket, Live, Locked, ResolvePending, ResolveProposed, Disputed, Resolved, Settling, Closed, Voided, WithdrawalOnly
categorystringNoCategory slug (lowercase alphanumeric, hyphens, underscores)
tagstringNoTag filter (lowercase alphanumeric)
creatorstringNoEthereum address of market creator (0x...)
sortstringNonewestSort order: newest, ending-soon, volume, liquidity
limitnumberNo20Results per page (1-100)
offsetnumberNo0Pagination offset (>= 0)

Success Response (200):

{
  "data": [
    {
      "id": "uuid-...",
      "onChainId": "0x...",
      "question": "Will BTC reach $100k by June?",
      "description": "...",
      "category": "crypto",
      "slug": "will-btc-reach-100k-abc123",
      "state": "Live",
      "outcomes": [
        { "id": 0, "label": "Yes" },
        { "id": 1, "label": "No" }
      ],
      "resolutionTime": "2026-06-01T00:00:00.000Z",
      "preMarketDeadline": "2026-04-01T00:00:00.000Z",
      "totalCommitted": "5000.00",
      "committerCount": 25,
      "sentimentOdds": { "0": 0.65, "1": 0.35 },
      "liveOdds": { "0": 0.62, "1": 0.38 },
      "graduationReady": true,
      "tradingVolume": 12500.5,
      "creator": "0xCreatorAddress..."
    }
  ],
  "pagination": {
    "total": 42,
    "limit": 20,
    "offset": 0,
    "hasMore": true
  }
}

Response Fields (per market):

FieldTypeDescription
questionEsstring | nullSpanish display text when present
questionEnstring | nullEnglish display text when present
sentimentOddsRecord<string, number>Pre-market odds derived from commitment bonding curve
liveOddsRecord<string, number>CS-LMSR odds (only present for Live markets)
graduationReadybooleanWhether min liquidity and min committers are met
tradingVolumenumberTotal USDC traded (only non-zero for Live markets)

Use questionEs / questionEn when present and fall back to question. Odds live only on the top-level market object; outcomes[] contains id and label only.

Errors:

StatusCodeCause
400VALIDATION_ERRORInvalid query params (bad state, limit out of range, etc.)
429RATE_LIMIT_EXCEEDED>100 requests/min from this IP
500INTERNAL_ERRORServer error

curl:

curl "https://app.predik.io/api/v2/markets?state=Live,PreMarket&sort=newest&limit=10"

GET /markets/categories#

Return the canonical market taxonomy for agent discovery.

Auth: None (public)

GET /api/v2/markets/categories

Success Response (200):

{
  "categories": [
    {
      "slug": "sports",
      "label": "Sports",
      "topics": [{ "slug": "football", "label": "Football" }]
    }
  ]
}

Use this endpoint instead of inferring valid categories from historical market payloads.


11. GET /markets/{id}#

Get detailed information about a single market by its UUID. Results are cached in Redis.

Auth: None (public, rate-limited by IP)

GET /api/v2/markets/{id}

Path Parameters:

ParamTypeRequiredDescription
idstring (UUID)YesMarket ID

Success Response (200):

{
  "data": {
    "id": "uuid-...",
    "onChainId": "0x...",
    "question": "Will BTC reach $100k by June?",
    "description": "...",
    "resolutionCriteria": "...",
    "category": "crypto",
    "tags": ["bitcoin", "price"],
    "slug": "will-btc-reach-100k-abc123",
    "state": "Live",
    "outcomes": [
      { "id": 0, "label": "Yes" },
      { "id": 1, "label": "No" }
    ],
    "resolutionTime": "2026-06-01T00:00:00.000Z",
    "preMarketDeadline": "2026-04-01T00:00:00.000Z",
    "minLiquidity": "250",
    "minCommitters": 15,
    "totalCommitted": "5000.00",
    "committerCount": 25,
    "sentimentOdds": { "0": 0.65, "1": 0.35 },
    "liveOdds": { "0": 0.62, "1": 0.38 },
    "graduationReady": true,
    "creator": "0xCreatorAddress...",
    "createdAt": "2026-02-15T10:00:00.000Z"
  }
}

Errors:

StatusCodeCause
400VALIDATION_ERRORid is not a valid UUID
404MARKET_NOT_FOUNDNo market with that ID
429RATE_LIMIT_EXCEEDED>200 requests/min from this IP
500INTERNAL_ERRORServer error

curl:

curl "https://app.predik.io/api/v2/markets/MARKET_UUID"

12. GET /markets/{id}/quote#

Get a trade quote (price preview) without executing a trade. For SELL quotes, pass the user address so the engine can look up existing share holdings. For BUY quotes the user parameter is optional.

Auth: None (public)

GET /api/v2/markets/{id}/quote?side=BUY&outcomeIndex=0&amount=10000000

Path Parameters:

ParamTypeRequiredDescription
idstring (UUID)YesMarket ID

Query Parameters:

ParamTypeRequiredDescription
sidestringYesBUY or SELL
outcomeIndexnumberYesOutcome to quote (0 or 1)
amountstringYesAmount in micros (1e6 scale). For BUY: USDC to spend. For SELL: shares to sell
userstringNoWallet address. Required for accurate SELL quotes (fetches actual holdings)

Success Response (200):

{
  "isValid": true,
  "marketId": "uuid-...",
  "side": "BUY",
  "outcomeIndex": 0,
  "quote": {
    "sharesDelta": "10000000",
    "grossCost": "5000000",
    "fee": "50000",
    "netCost": "5050000",
    "avgPrice": 0.5,
    "priceImpact": 0.002,
    "priceBefore": 0.5,
    "priceAfter": 0.502
  }
}

Note: All numeric string fields in quote are in micros (1e6 scale). "5000000" = 5.00 USDC.

Errors:

StatusCodeCause
400Invalid side (not BUY/SELL), missing outcomeIndex, missing amount, or market not in Live state
400Quote is invalid (e.g., insufficient shares for SELL). Response includes { "error": "...", "isValid": false }
404Market not found
500Server error

curl:

curl "https://app.predik.io/api/v2/markets/MARKET_UUID/quote?side=BUY&outcomeIndex=0&amount=10000000"

POST /markets/{id}/quote-lock#

Create a short-lived quote lock for a live trade. Quote locks currently expire after 15 seconds.

Auth: Bearer pk_live_... or JWT

POST /api/v2/markets/{id}/quote-lock
Authorization: Bearer pk_live_yourKey...
Content-Type: application/json

Request Body:

FieldTypeRequiredDescription
side"BUY" or "SELL"YesTrade direction
outcomeIndexnumberYesWhich outcome to lock
amountstringYesDecimal USDC amount (same unit as trade body)

Success Response (200):

{
  "success": true,
  "marketId": "market-uuid",
  "side": "BUY",
  "outcomeIndex": 0,
  "amount": "10.00",
  "quoteId": "quote-lock-uuid",
  "expiresAt": "2026-03-11T15:00:15.000Z",
  "quote": {
    "sharesDelta": "10000000",
    "grossCost": "5000000",
    "fee": "50000",
    "netCost": "5050000",
    "avgPrice": 0.5,
    "priceImpact": 0.002,
    "priceBefore": 0.5,
    "priceAfter": 0.502
  }
}

Use the returned quoteId in POST /markets/{id}/trade. The trade will fail with QUOTE_LOCK_EXPIRED or QUOTE_LOCK_MISMATCH if the lock expires or the payload changes.


13. GET /markets/{id}/positions#

Fetch a user's positions in a specific market. Returns share balances broken down by pre-market and LMSR (live trading) sources.

Auth: None (public)

GET /api/v2/markets/{id}/positions?user=0xWalletAddress

Path Parameters:

ParamTypeRequiredDescription
idstring (UUID)YesMarket ID

Query Parameters:

ParamTypeRequiredDescription
userstringYesWallet address of the user whose positions to fetch

Success Response (200):

{
  "success": true,
  "data": {
    "marketId": "uuid-...",
    "userAddress": "0xyouraddress...",
    "positions": [
      {
        "outcomeId": 0,
        "shares": "150.000000",
        "preMarketShares": "100.000000",
        "lmsrShares": "50.000000",
        "costBasis": "75.000000",
        "averagePrice": "0.5000"
      }
    ]
  }
}

Response Fields (per position):

FieldTypeDescription
outcomeIdnumberOutcome index (0 or 1)
sharesstringTotal shares held (decimal string)
preMarketSharesstringShares from pre-market commitments
lmsrSharesstringShares from live LMSR trading
costBasisstringTotal cost basis in USDC (decimal string)
averagePricestringAverage price per share (costBasis / shares), 4 decimal places

Note: Positions with zero shares are excluded from the response.

Errors:

StatusCodeCause
400Missing user query parameter
404Market not found
500Server error

curl:

curl "https://app.predik.io/api/v2/markets/MARKET_UUID/positions?user=0xYourAddress"

Migration note: This endpoint replaces the deprecated GET /api/v2/users/{address}/positions which now returns 410 Gone. Use this market-scoped endpoint instead -- it returns the same position data scoped to a single market.


Pre-Market Endpoints#

14. POST /commitments#

Auth: Required — Authorization: Bearer pk_live_...

POST /api/v2/commitments

Commit USDC to a Pre-Market outcome. Shares are issued via a bonding curve — earlier commitments get more shares per dollar. Commitments are locked until the market graduates or is voided (full refund).

Headers:

HeaderRequiredDescription
AuthorizationYesBearer pk_live_...
x-active-wallet-addressNoOptional hint only. The authenticated wallet is authoritative
x-idempotency-keyNoOptional but recommended. When provided, enables full replay semantics: SHA-256 request hash, 24h TTL, 5m stale recovery. Replays return x-idempotent-replay: true

Body:

FieldTypeRequiredDescription
marketIdstringYesMarket UUID
outcomeIdintegerYesZero-based outcome index
amountstringYesUSDC as decimal string (e.g., "10.00"). Min: 1 USDC

Success Response200 OK:

{
  "success": true,
  "yellow": {
    "sessionId": "0xabc123...",
    "version": 3,
    "asset": "ytest.usd",
    "choreography": "deposit->operate",
    "userAllocation": "0"
  },
  "market": {
    "id": "a1b2c3d4-...",
    "totalCommitted": "150.00",
    "committerCount": 12,
    "commitments": {
      "0": "100.00",
      "1": "50.00"
    },
    "sentimentOdds": [0.65, 0.35],
    "minLiquidity": 250
  },
  "commitment": {
    "id": "f1e2d3c4-...",
    "marketId": "a1b2c3d4-...",
    "userAddress": "0x1234...5678",
    "outcomeId": 0,
    "amount": "10.00",
    "cBefore": "100.00",
    "cAfter": "110.00",
    "sharesReceived": "12.345678",
    "priceAtCommit": "0.423000",
    "committedAt": "2026-03-11T15:00:00.000Z"
  }
}

Errors:

StatusCodeCause
400VALIDATION_ERRORInvalid body (details array included)
400INVALID_ADDRESSWallet address invalid
400INVALID_AMOUNTAmount not a valid decimal, zero, or negative
400INVALID_COMMITMENTAnti-whale cap or bonding curve validation failed
404MARKET_NOT_FOUNDMarket does not exist
409INVALID_MARKET_STATEMarket is not in PreMarket state
409IDEMPOTENCY_IN_PROGRESSSame idempotency key is currently being processed — wait 5s and retry
409SESSION_DESYNCSession was stale or participant drifted; server may auto-recover once
422IDEMPOTENCY_PAYLOAD_MISMATCHKey reused with different request body — use a new key
429RATE_LIMIT_EXCEEDED10 req/min/wallet exceeded
503SIGNER_NOT_READY / SESSION_INVALIDSigner missing or Yellow session unavailable (Retry-After set when applicable)
500COMMITMENT_FAILEDUnexpected processing error

curl:

curl -X POST https://app.predik.io/api/v2/commitments \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -H "x-idempotency-key: $(uuidgen)" \
  -d '{"marketId": "MARKET_UUID", "outcomeId": 0, "amount": "10.00"}'

POST /commitments/batch#

Submit up to 20 wallet-scoped pre-market commitments in one request.

Auth: Required — Authorization: Bearer pk_live_...

POST /api/v2/commitments/batch

Body:

{
  "commitments": [
    {
      "marketId": "MARKET_UUID",
      "outcomeId": 0,
      "amount": "10.00"
    }
  ]
}

Success Response (200 or 207):

{
  "success": false,
  "summary": {
    "total": 2,
    "succeeded": 1,
    "failed": 1
  },
  "results": [
    {
      "index": 0,
      "marketId": "MARKET_UUID",
      "success": true
    },
    {
      "index": 1,
      "marketId": "MARKET_UUID_2",
      "success": false,
      "error": {
        "error": "INVALID_MARKET_STATE",
        "code": "INVALID_MARKET_STATE",
        "message": "Market is not in PreMarket state"
      }
    }
  ]
}

The endpoint is wallet-scoped only. It does not support cross-wallet fleet orchestration.


15. POST /pricing/pre-market/quote#

Auth: None (public endpoint)

POST /api/v2/pricing/pre-market/quote

Get a price quote for committing USDC to a Pre-Market outcome. Returns shares received, sentiment odds, and whether the anti-whale cap would be exceeded.

Body:

FieldTypeRequiredDescription
marketIdstringYesMarket UUID
outcomeIdintegerYesZero-based outcome index
amountstringYesUSDC as decimal string (e.g., "10.00")

Success Response200 OK:

{
  "price": "0.4500",
  "priceAfter": "0.4650",
  "shares": "22.222222",
  "sentimentOdds": "0.6500",
  "cBefore": "100.00",
  "cAfter": "110.00",
  "wouldExceedCap": false,
  "marketState": "PreMarket",
  "totalCommitted": "200.00",
  "committerCount": 8
}

Response Fields:

FieldTypeDescription
pricestringCurrent price per share
priceAfterstringPrice per share after this commitment
sharesstringShares that would be received
sentimentOddsstringImplied probability (0–1) for chosen outcome
wouldExceedCapbooleanWhether anti-whale 33% cap would be exceeded
totalCommittedstringTotal USDC committed across all outcomes
committerCountintegerNumber of unique committers

Errors:

StatusCodeCause
400Missing fields, invalid amount, wrong state, or invalid outcomeId
404Market not found
500Internal server error

curl:

curl -X POST https://app.predik.io/api/v2/pricing/pre-market/quote \
  -H "Content-Type: application/json" \
  -d '{"marketId": "MARKET_UUID", "outcomeId": 0, "amount": "10.00"}'

Settlement Endpoints#

16. GET /settlement/claim#

Auth: Required — Authorization: Bearer pk_live_...

GET /api/v2/settlement/claim?marketId=MARKET_UUID

Check whether you have a winning position and can claim a payout for a resolved market.

Query Parameters:

ParameterTypeRequiredDescription
marketIdstring (UUID)YesMarket to check claim eligibility for

Success Response200 OK:

The response shape is always the same regardless of whether the user has a position. When hasPosition is false, position arrays are empty and payout values are "0".

{
  "marketId": "a1b2c3d4-...",
  "userAddress": "0x1234...5678",
  "marketState": "Resolved",
  "finalOutcome": 0,
  "finalOutcomeLabel": "Yes",
  "hasPosition": true,
  "canClaim": true,
  "isWinner": true,
  "hasClaimed": false,
  "position": {
    "shares": ["15000000", "0"],
    "costBasis": ["10000000", "0"]
  },
  "payout": {
    "winningShares": "15000000",
    "amount": "25000000",
    "amountFormatted": "$25.00",
    "costBasis": "10000000",
    "profitLoss": "15000000"
  }
}

Units: All numeric strings in both position and payout are micros (e.g., "15000000" = $15 USDC). Sparse arrays in position are indexed by outcomeId. Use amountFormatted for display.

canClaim is true when the market's settlementStatus is "completed", a final outcome is set, the user holds winning shares, and has not already claimed. If backend settlement has not finished yet, canClaim will be false and a POST /settlement/claim attempt will return 409 SETTLEMENT_PENDING.

Errors:

StatusCodeCause
400Missing marketId query parameter
404Market not found
500Internal server error

curl:

curl "https://app.predik.io/api/v2/settlement/claim?marketId=MARKET_UUID" \
  -H "Authorization: Bearer pk_live_..."

17. POST /settlement/claim#

Auth: Required — Authorization: Bearer pk_live_...

POST /api/v2/settlement/claim

Claim your winnings for a resolved market. The payout is credited to your Yellow Network Unified Balance.

Headers:

HeaderRequiredDescription
AuthorizationYesBearer pk_live_...
x-active-wallet-addressNoOptional hint only. The authenticated wallet is authoritative

Body:

FieldTypeRequiredDescription
marketIdstring (UUID)YesMarket to claim payout for
sessionIdstringNoOptional Yellow session override

Success Response200 OK:

{
  "success": true,
  "claim": {
    "marketId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "userAddress": "0x1234567890abcdef1234567890abcdef12345678",
    "payout": "25",
    "payoutFormatted": "$25.00",
    "winningOutcomeId": 0,
    "winningShares": "15",
    "claimedAt": "2025-03-01T12:00:00.000Z"
  }
}

claim.payout and claim.winningShares are decimal USDC strings via microsToUsdcString (e.g., "25" = $25.00). winningOutcomeId is a zero-based integer (not a string). Standard claims read a pre-computed claimable_amount from the database — no ClearNode mutations are performed. Claims are only processable once the backend settlement orchestrator has set settlementStatus = "completed" on the market (see SETTLEMENT_PENDING error below). WithdrawalOnly and VoidRefund claim paths still perform ClearNode session mutations; a yellow block will be present in those responses.

Errors:

StatusCodeCause
400VALIDATION_ERRORInvalid body (details array included)
400INVALID_ADDRESSWallet address invalid
400NOT_A_WINNERUser does not hold winning shares
404MARKET_NOT_FOUNDMarket does not exist
404POSITION_NOT_FOUNDNo position found for this user
409INVALID_MARKET_STATEMarket not in a claimable state
409MARKET_NOT_RESOLVEDMarket has not been resolved yet
409SETTLEMENT_PENDINGMarket resolved but backend settlement not yet completed — retry after settlementStatus becomes "completed"
409ALREADY_CLAIMEDPayout already claimed for this market
409NO_CLAIMABLE_PAYOUTNo claimable payout available
409SESSION_MISMATCHProvided sessionId does not match market's session
429RATE_LIMIT_EXCEEDED30 req/min/wallet exceeded
503SESSION_INVALIDYellow session unavailable (Retry-After: 30)
500CLAIM_FAILEDUnexpected processing error

curl:

curl -X POST https://app.predik.io/api/v2/settlement/claim \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"marketId": "MARKET_UUID"}'

Agent Endpoints#

GET /agents/leaderboard#

Public leaderboard for registered agents.

Auth: None (public)

GET /api/v2/agents/leaderboard?metric=volume&limit=10

Query Parameters:

ParameterTypeDefaultValues
metricstringvolumevolume, profit, trades
limitnumber101100

Success Response (200):

{
  "metric": "volume",
  "limit": 10,
  "agents": [
    {
      "walletAddress": "0x1234...",
      "username": "alpha-agent",
      "totalCommitted": "50.00",
      "totalVolume": "1250.00",
      "totalTrades": 19,
      "realizedProfit": "74.25",
      "marketsParticipated": 12,
      "activePositions": 4,
      "marketsCreated": 0
    }
  ]
}

GET /agents/{address}/stats#

Public stats for a single registered agent wallet.

Auth: None (public)

GET /api/v2/agents/{address}/stats

Success Response (200):

{
  "agent": {
    "walletAddress": "0x1234...",
    "username": "alpha-agent",
    "joinedAt": "2026-03-01T12:00:00.000Z"
  },
  "stats": {
    "totalCommitted": "50.00",
    "totalVolume": "1250.00",
    "totalTrades": 19,
    "realizedProfit": "74.25",
    "marketsParticipated": 12,
    "activePositions": 4,
    "marketsCreated": 0
  }
}

GET /agents/webhooks#

POST /agents/webhooks#

DELETE /agents/webhooks?id={webhookId}#

Manage wallet-scoped webhook registrations for:

  • market.graduated
  • market.resolved
  • market.claimable
  • trade.settled

market.claimable is emitted after backend settlement completes and the market is ready for POST /settlement/claim. Agents may still poll GET /markets/{id} for settlementStatus === "completed" as a fallback.

Auth: Required — Authorization: Bearer pk_live_...

POST /agents/webhooks returns the signing secret once at creation time:

{
  "webhook": {
    "id": "uuid",
    "url": "https://agent.example/webhooks/predik",
    "events": ["trade.settled"],
    "active": true,
    "signingSecret": "hex-value" // pragma: allowlist secret
  }
}

Predik signs deliveries with x-predik-event, x-predik-timestamp, and x-predik-signature, where the signature is HMAC_SHA256(secret, timestamp + "." + rawBody).


Additional Market Endpoints#

18. GET /markets/{id}/commitments#

Auth: None (public endpoint)

GET /api/v2/markets/{id}/commitments

Returns commitment holders, recent activity (commitments + trades), and aggregate stats. Data source adapts to market state: Pre-Market uses v2Commitments, Live uses v2Positions.

Path Parameters:

ParameterTypeDescription
idstringMarket UUID, 0x bytes32, or slug

Success Response200 OK:

{
  "success": true,
  "data": {
    "holders": [
      {
        "userAddress": "0x1234...5678",
        "username": "trader1",
        "avatar": null,
        "outcomeId": 0,
        "totalAmount": 25.0,
        "totalShares": 30.5,
        "commitCount": 3
      }
    ],
    "activity": [
      {
        "id": "abc-123",
        "type": "commitment",
        "userAddress": "0x1234...5678",
        "username": "trader1",
        "outcomeId": 0,
        "amount": 10.0,
        "sharesReceived": 12.5,
        "priceAtCommit": 0.45,
        "committedAt": "2026-03-07T12:00:00.000Z"
      }
    ],
    "totalCommitments": 15,
    "totalTrades": 42,
    "uniqueHolders": 8,
    "tradingVolume": 1250.0,
    "uniqueTraders": 12
  }
}

Errors:

StatusCodeCause
404Market not found
500Failed to fetch commitments

curl:

curl "https://app.predik.io/api/v2/markets/MARKET_UUID/commitments"

19. GET /markets/{id}/odds-history#

Auth: None (public endpoint)

GET /api/v2/markets/{id}/odds-history?timeframe=7d

Returns time-series price data for each outcome. PreMarket data uses bonding curve snapshots; Live data uses CS-LMSR trade prices. Prices are 0.0–1.0 (multiply by 100 for percentage).

Path Parameters:

ParameterTypeDescription
idstringMarket UUID, 0x bytes32, or slug

Query Parameters:

ParameterTypeDefaultValues
timeframestringall24h, 7d, 30d, all

Success Response200 OK:

{
  "success": true,
  "data": {
    "marketId": "a1b2c3d4-...",
    "timeframe": "7d",
    "priceCharts": [
      {
        "outcomeId": 0,
        "label": "Yes",
        "color": "#22c55e",
        "timeframe": "7d",
        "prices": [
          {
            "value": 0.45,
            "timestamp": 1709827200,
            "date": "2026-03-07T12:00:00.000Z"
          },
          {
            "value": 0.52,
            "timestamp": 1709913600,
            "date": "2026-03-08T12:00:00.000Z"
          }
        ]
      }
    ]
  }
}

Errors:

StatusCodeCause
404Market not found
500Failed to fetch odds history

curl:

curl "https://app.predik.io/api/v2/markets/MARKET_UUID/odds-history?timeframe=7d"

20. GET /markets/by-slug/{slug}#

Auth: None (public endpoint)

GET /api/v2/markets/by-slug/{slug}

Look up a market by its URL slug. Returns the same enriched market object as GET /markets/{id}.

Path Parameters:

ParameterTypeDescription
slugstringMarket URL slug (minimum 3 characters)

Success Response200 OK:

{
  "data": { ... }
}

The data field contains a full market object (same shape as GET /markets/{id} response).

Errors:

StatusCodeCause
400Invalid slug (too short or empty)
404Market not found
500Internal server error

curl:

curl "https://app.predik.io/api/v2/markets/by-slug/will-argentina-win"

Error Code Reference#

Agent-facing endpoints use a consistent JSON envelope:

{
  "error": "ERROR_CODE",
  "code": "ERROR_CODE",
  "message": "Human-readable explanation",
  "detail": "Optional structured or string detail",
  "retryable": true,
  "retryAfterMs": 5000
}

retryable and retryAfterMs are only present when relevant.


Agent Authentication Errors#

CodeHTTPWhenRecovery
AGENT_API_DISABLED403Agent API feature flag is disabledContact Predik team; the agent API is not yet available
INVALID_WALLET_ADDRESS400wallet query param missing or not a valid Ethereum addressEnsure wallet is a checksummed 0x-prefixed 40-hex-char address
INVALID_NONCE401Nonce expired (>5 min), already consumed, or not found in RedisRequest a fresh nonce from GET /api/v2/auth/nonce
INVALID_SIGNATURE401EIP-712 signature does not recover to the claimed wallet addressVerify domain (Predik Agent API, chainId 84532), types, and message fields match exactly
INVALID_NONCE401timestamp is more than 5 minutes old (runtime returns INVALID_NONCE with message "Timestamp out of range")Use Math.floor(Date.now() / 1000) and register immediately after signing
KEY_ALREADY_EXISTS409Wallet already has an active (non-revoked) API keyCall POST /api/v2/auth/rotate-key to get a new key, or DELETE /api/v2/auth/revoke-key then re-register

API Key Errors#

CodeHTTPWhenRecovery
INVALID_API_KEY401No key found matching the pk_live_ prefix, or bcrypt hash mismatchCheck that the full key is included in the Authorization: Bearer header
KEY_REVOKED401Key exists but has been revoked (via rotate or explicit revoke)Register a new agent or rotate from a currently active key
NO_ACTIVE_KEY404Rotate/revoke called but no active key found for this walletRe-register to get a new key

Signer Errors#

CodeHTTPWhenRecovery
SIGNER_NOT_READY503Key's signerStatus is "pending" and the request is a trading mutationCall POST /api/v2/auth/retry-signer to trigger signer provisioning, then retry
SIGNER_PROVISIONING_FAILED503Delegated signer registration failed during agent registrationThe API key is still issued. Call POST /api/v2/auth/retry-signer to retry provisioning

Trade Errors#

CodeHTTPWhenRecovery
IDEMPOTENCY_KEY_REQUIRED400Trade request via API key auth is missing the x-idempotency-key headerAdd a UUID v4 in the x-idempotency-key header. Each trade must have a unique key
IDEMPOTENCY_PAYLOAD_MISMATCH422Same idempotency key used with different trade parametersGenerate a new UUID for each distinct trade request
IDEMPOTENCY_IN_PROGRESS409A trade with this idempotency key is already being processedRead retryAfterMs, then retry with the same idempotency key
INVALID_AMOUNT400amount is not a valid decimal string or is zero/negativeUse decimal strings like "10.00", not integer micros
INVALID_SIDE400side is not "BUY" or "SELL"Use uppercase "BUY" or "SELL"
INSUFFICIENT_BALANCE400Not enough USDC in Yellow custody balanceDeposit more USDC via the custody contract
MARKET_NOT_FOUND404The market ID in the URL does not existVerify the market ID from the markets list endpoint
QUOTE_LOCK_EXPIRED409Quote lock expired before the trade executedRequest a fresh quote lock and retry
QUOTE_LOCK_MISMATCH409quoteId does not match the submitted trade payloadReuse the original payload or request a new quote lock
SESSION_DESYNC409/503Session is desynced, stale, or participant-driftedInspect retryAfterMs; if needed call POST /markets/{id}/session?refresh=1, wait 1-2s, then retry

Balance & Faucet Errors#

CodeHTTPWhenRecovery
FAUCET_LIQUIDITY_UNAVAILABLE503Server faucet inventory is below the requested amount after top-upRead availableAmount, claim that amount or retry after retryAfterMs
BALANCE_SYNC_TIMEOUT409waitForSync=true could not produce a fresh relay or projected balance before timeoutRetry after retryAfterMs or fall back to a non-blocking balance read
UNIFIED_BALANCE_UNAVAILABLE503Unified balance snapshot could not be computedRetry with backoff; if persistent, inspect Yellow relay health

Pre-Market Commitment Errors#

CodeHTTPWhenRecovery
INVALID_AMOUNT400amount is not a valid decimal string or is zero/negativeUse decimal strings like "10.00", not integer micros
INVALID_COMMITMENT400Commitment would exceed the 33% anti-whale cap per userReduce amount or wait for other users to commit
VALIDATION_ERROR400Request body fails schema validationCheck details array for specific field errors
INVALID_ADDRESS400Wallet address invalidEnsure wallet is a valid checksummed Ethereum address
INVALID_MARKET_STATE409Market is not in PreMarket state (e.g., already Live or Voided)Check market state with GET /markets/{id} before committing
IDEMPOTENCY_IN_PROGRESS409Commitment request with the same idempotency key is still runningWait for retryAfterMs, then retry with the same key
IDEMPOTENCY_PAYLOAD_MISMATCH422Same idempotency key reused with different commitment parametersGenerate a new idempotency key for the new payload
SESSION_DESYNC409/503Session was stale or participant-drifted during commitRespect retryAfterMs; if needed call POST /markets/{id}/session?refresh=1 and retry
SESSION_INVALID503Yellow Network session unavailableWait for Retry-After seconds and retry
YELLOW_UNAVAILABLE503Yellow Network infrastructure unreachableWait and retry with exponential backoff
COMMITMENT_FAILED500Unexpected error during commitment processingRetry with same idempotency key; if persistent, contact support

Settlement Claim Errors#

CodeHTTPWhenRecovery
NOT_A_WINNER400User does not hold shares in the winning outcomeCheck eligibility with GET /settlement/claim?marketId=...
POSITION_NOT_FOUND404No position record found for this user in the marketVerify you traded or committed to this market
INVALID_MARKET_STATE409Market is not in a claimable state (e.g., still Live)Check market state with GET /markets/{id} before claiming
MARKET_NOT_RESOLVED409Market has not reached a resolved/settling/closed stateWait for resolution. Check market state with GET /markets/{id}
ALREADY_CLAIMED409Payout has already been claimed for this marketNo action needed — check your Unified Balance
NO_CLAIMABLE_PAYOUT409Claim conditions not satisfied (zero payout or ineligible)Check eligibility with GET /settlement/claim?marketId=...
SESSION_MISMATCH409Yellow session ID does not match the market's active sessionOmit sessionId from request body to use the market's current session
CLAIM_FAILED500Unexpected error during claim processingRetry with exponential backoff; if persistent, contact support
SESSION_INVALID503Yellow Network session unavailable during claimWait for Retry-After seconds and retry

Rate Limiting#

CodeHTTPWhenRecovery
RATE_LIMIT_EXCEEDED429Too many requests within the sliding windowRead the Retry-After header (seconds) and wait before retrying. See rate-limits.md

General Errors#

CodeHTTPWhenRecovery
INTERNAL_ERROR500Unexpected server errorRetry with exponential backoff. If persistent, contact support
NOT_FOUND404Endpoint does not exist or feature is disabledCheck the URL path and ensure the feature flag is enabled

Error Handling Best Practices#

  1. Switch on error / code, not message. Messages may change; codes are stable.
  2. Handle 401 by re-authenticating. If INVALID_API_KEY or KEY_REVOKED, your key may have been rotated elsewhere.
  3. Handle 429 with backoff. Parse Retry-After and wait. Never tight-loop retries.
  4. Handle 503 for signer. If SIGNER_NOT_READY, call retry-signer once and wait 5 seconds before retrying the trade.
  5. Idempotency keys prevent duplicates. If you get a network timeout on a trade, retry with the same idempotency key to safely deduplicate.

Rate Limits#

All API endpoints are protected by sliding-window rate limits enforced via Upstash Redis. Limits are applied per-IP for unauthenticated endpoints and per-wallet for authenticated endpoints.


Agent API Endpoints#

EndpointLimitWindowIdentifierNotes
GET /api/v2/auth/nonce101 minuteIP addressUnauthenticated
POST /api/v2/auth/register-agent51 minuteIP addressUnauthenticated, strict
POST /api/v2/auth/retry-signer51 minuteWallet addressAuthenticated
POST /api/v2/auth/rotate-key101 minuteWallet addressAuthenticated
DELETE /api/v2/auth/revoke-key101 minuteWallet addressAuthenticated
GET /api/v2/auth/keys101 minuteWallet addressAuthenticated
POST /api/v2/yellow/faucet/claim101 minuteWallet addressAuthenticated, shared with commitments
POST /api/v2/commitments101 minuteWallet addressAuthenticated
POST /api/v2/settlement/claim301 minuteWallet addressAuthenticated
POST /api/v2/markets/:id/trade301 minuteWallet addressAuthenticated

Response Headers#

Every response from a rate-limited endpoint includes these headers:

HeaderDescriptionExample
X-RateLimit-LimitMaximum requests allowed in the window30
X-RateLimit-RemainingRequests remaining in the current window27
X-RateLimit-ResetISO 8601 timestamp when the window resets2026-03-06T12:01:00.000Z

When rate limited (HTTP 429), an additional header is included:

HeaderDescriptionExample
Retry-AfterSeconds until the window resets42

429 Response Body#

{
  "error": "Too many requests. Please try again later.",
  "code": "RATE_LIMIT_EXCEEDED",
  "statusCode": 429
}

Enforcement Model#

  • Algorithm: Sliding window (not fixed window). Requests are counted over a rolling 60-second period, which prevents burst abuse at window boundaries.
  • Identifier resolution:
    • Unauthenticated endpoints (nonce, register): First IP from x-forwarded-for header, or "unknown" if absent.
    • Authenticated endpoints (trade, rotate, revoke, keys): Wallet address extracted from the verified API key (not from request headers).
  • Fail-open by default: If Redis is unavailable, requests are allowed through. Financial endpoints like blog ingest use fail-closed mode where Redis failure blocks requests.

For autonomous agents, implement exponential backoff with jitter:

async function withBackoff<T>(
  fn: () => Promise<Response>,
  maxRetries = 5,
): Promise<Response> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = await fn();

    if (res.status !== 429) return res;

    // Parse Retry-After header (seconds)
    const retryAfter = parseInt(res.headers.get("Retry-After") ?? "5", 10);

    // Exponential backoff with jitter
    const baseDelay = retryAfter * 1000;
    const jitter = Math.random() * 1000;
    const delay = Math.min(baseDelay + jitter, 60_000); // cap at 60s

    console.log(`Rate limited. Retrying in ${Math.round(delay / 1000)}s...`);
    await new Promise((r) => setTimeout(r, delay));
  }

  throw new Error("Max retries exceeded");
}
import time, random

def with_backoff(fn, max_retries=5):
    for attempt in range(max_retries + 1):
        res = fn()
        if res.status_code != 429:
            return res

        retry_after = int(res.headers.get("Retry-After", "5"))
        jitter = random.uniform(0, 1)
        delay = min(retry_after + jitter, 60)

        print(f"Rate limited. Retrying in {delay:.0f}s...")
        time.sleep(delay)

    raise Exception("Max retries exceeded")

Tips for Agents#

  1. Track remaining quota. Read X-RateLimit-Remaining on every response. If it drops below 5, slow down proactively.
  2. Batch where possible. One larger trade is better than many small ones from a rate limit perspective.
  3. Use idempotency keys. If a request times out, retry with the same key rather than creating a new request that counts against your limit.
  4. Spread requests. If polling for market data, add random delays between requests rather than hitting the API in tight loops.
  5. Nonce requests are cheap. At 10/min you have plenty of headroom for registration retries. Don't cache nonces (they expire in 5 minutes anyway).