API v1
faro.markets GitHub Contact

The Faro API

A B2B HTTP API that unifies prediction markets across Polymarket and Kalshi (via DFlow) into a single read & trade surface. Faro is the routing, matching, and data layer — never custody.

Unified events Cross-venue quotes Non-custodial REST JSON

Base URL

BASE URL
https://api.faro.markets/api/v1

What you can build

  • Data — list and query unified events, with description and timeline coming from the highest-liquidity venue at match time.
  • Trading — non-custodial pass-through routing: clients sign their own orders, Faro plans the route and submits.
  • Matching — a daily LLM run pairs markets that resolve identically; only matches with confidence ≥ 0.85 are linked.

Quick start

Hit the public liveness endpoint to confirm reachability, then use a Faro-issued API key for everything else.

cURL — Liveness
curl https://api.faro.markets/api/v1/health
cURL — Authenticated
curl https://api.faro.markets/api/v1/events \
  -H "Authorization: Bearer faro_live_<your-secret>"

Authentication

All /api/v1/* routes (except /health) require a Faro-issued API key passed as a Bearer token.

HEADER
Authorization: Bearer faro_live_<secret>

Scopes

ScopeGrants
readAll read endpoints (events, quotes, orderbook, markets, positions, balances).
tradeOrder preparation, submission, cancellation. Implies read.
adminKey management endpoints. Reserved for ops.

Issuing keys

Keys are minted via the admin endpoint, gated by ADMIN_SECRET. The plaintext key is returned once and is unrecoverable after that.

cURL — Issue key
curl -X POST https://api.faro.markets/api/v1/admin/keys \
  -H "Authorization: Bearer $ADMIN_SECRET" \
  -H "content-type: application/json" \
  -d '{"label":"acme-corp","scopes":["read","trade"],"rate_limit_per_min":120}'
Treat keys like passwords

Keys are stored hashed. We cannot recover a lost plaintext — revoke and re-issue.

Errors

All errors are returned as JSON with a stable error code and a human-readable message. Validation errors include a structured issues object from Zod.

StatusCodeMeaning
400bad_requestMalformed input or invalid query parameters.
401unauthorizedMissing, expired, or revoked API key.
403forbiddenAPI key lacks the required scope.
404not_foundResource does not exist or is not visible to your key.
429rate_limitedPer-minute key budget exceeded. Retry after the window.
502upstreamPolymarket / Kalshi / DFlow returned an error.
500internalUnhandled server error.
JSON — Error shape
{
  "error": "bad_request",
  "message": "Invalid request body",
  "issues": {
    "fieldErrors": { "size_usd": ["Number must be positive"] }
  }
}

Rate limits

Each API key has a per-minute budget set at issue time (default 60, max 100,000). The window is rolling and per-key. When you hit the cap you'll receive 429 rate_limited; the response includes the standard Retry-After header.

Need a higher cap? Contact admin@faro.markets with your expected request profile.

Custody model

Pass-through, non-custodial. Faro never holds funds, never signs orders, never custodies anything. Clients hold:

  • A Polygon EOA + USDC + Polymarket CLOB credentials for the Polymarket leg.
  • A Solana keypair + USDC for the Kalshi leg. Kalshi outcomes are SPL tokens on Solana; the swap is built via Jupiter v6, which routes through DFlow's RFQ liquidity.
Two-step trade flow

Call POST /orders/prepare to receive a route plan plus per-leg unsigned payloads (EIP-712 typed-data for Polymarket, base64 versioned Solana transactions for Kalshi). Sign each leg locally, then call POST /orders/submit to relay them.

How the Kalshi leg actually executes

DFlow exposes two surfaces:

  • dev-prediction-markets-api.dflow.net — Kalshi data wrapper. Read-only.
  • quote-api.dflow.net — DFlow's swap-execution API. Permissioned (returns 403 without partner credentials).

Faro doesn't require DFlow partner access. Every Kalshi outcome is tokenized as a Solana SPL mint, and Jupiter v6 is a public Solana router that already routes through DFlow's RFQ liquidity as one of its sources. So POST /v1/orders/prepare returns a base64 versioned Solana transaction at legs[i].unsigned_payload.transaction_b64; the client signs locally with their Solana keypair and either submits via their own RPC or POSTs signed_tx_b64 to /v1/orders/submit for Faro to relay.


Liveness

Public liveness probe. Useful as a sanity check from CI, monitors, or a health dashboard.

GET /api/v1/health Public

Response — 200

JSON
{
  "ok": true,
  "ts": "2026-05-09T12:34:56.789Z"
}

List events

Paginated list of unified events. The DTO blends per-venue listings into a single concept; description and timeline come from the venue with the highest liquidity at match time.

GET /api/v1/events read

Query parameters

NameTypeDescription
limitoptintegerPage size, 1–200. Default 50.
offsetoptintegerPagination offset. Default 0.
statusopt"active" | "settled" | "all"Defaults to active.
end_beforeoptISO 8601Only events whose end-time is before the given timestamp.
qoptstringFull-text search across event title and description.

Example request

cURL
curl "https://api.faro.markets/api/v1/events?limit=20&status=active&q=election" \
  -H "Authorization: Bearer $FARO_KEY"

Response — 200

JSON
{
  "events": [
    {
      "id": "7c6a…",
      "title": "Will X happen by Dec 31?",
      "category": "politics",
      "end_time": "2026-12-31T23:59:00Z",
      "venues": ["polymarket", "kalshi"],
      "primary_venue": "polymarket",
      "liquidity_usd": 421300
    }
  ],
  "total": 1248,
  "limit": 20,
  "offset": 0
}

Get event

Returns the full unified event, including both venue listings (when matched) and the primary venue's description and timeline.

GET /api/v1/events/{id} read

Path parameters

NameTypeDescription
idrequireduuidUnified event id.

Example request

cURL
curl https://api.faro.markets/api/v1/events/7c6a… \
  -H "Authorization: Bearer $FARO_KEY"

Response — 200

JSON
{
  "event": {
    "id": "7c6a…",
    "title": "Will X happen by Dec 31?",
    "description": "…drawn from primary venue…",
    "markets": [
      { "venue": "polymarket", "venue_market_id": "0x…" },
      { "venue": "kalshi",     "venue_market_id": "X-Y-Z" }
    ],
    "match": { "confidence": 0.94, "reasoning": "…" }
  }
}

Event quotes

Best bid/ask per outcome, per venue, derived from each venue's live order book at the moment of the request.

GET /api/v1/events/{id}/quotes read

Path parameters

NameTypeDescription
idrequireduuidUnified event id.

Response — 200

JSON
{
  "unified_event_id": "7c6a…",
  "quotes": [
    {
      "venue": "polymarket",
      "venue_market_id": "0x…",
      "yes": { "bid": 0.62, "ask": 0.64 },
      "no":  { "bid": 0.36, "ask": 0.38 }
    },
    {
      "venue": "kalshi",
      "venue_market_id": "X-Y-Z",
      "yes": { "bid": 0.61, "ask": 0.65 },
      "no":  { "bid": 0.35, "ask": 0.39 }
    }
  ]
}
Per-leg errors are not fatal

If a single venue's order book fails to fetch, that entry is returned with an error field instead of yes/no. Other venues still resolve.

Orderbook

Full venue order book for a single outcome. Use the quotes endpoint for top-of-book; use this when you need depth.

GET /api/v1/events/{id}/orderbook read

Query parameters

NameTypeDescription
venuerequired"polymarket" | "kalshi"Which venue's book to return.
outcomeopt"YES" | "NO"Defaults to YES.

Response — 200

JSON
{
  "venue": "polymarket",
  "outcome": "YES",
  "book": {
    "bids": [{ "price": 0.62, "size": 1200 }],
    "asks": [{ "price": 0.64, "size": 800 }]
  }
}

Raw markets

Per-venue, unmatched feed of every active market we've seen. Useful for analytics, ETLs, or building your own matcher.

GET /api/v1/markets/raw read

Query parameters

NameTypeDescription
venuerequired"polymarket" | "kalshi"Which feed to return.
limitoptintegerPage size, 1–200. Default 50.
offsetoptintegerPagination offset. Default 0.

Response — 200

JSON
{
  "venue": "polymarket",
  "limit": 50,
  "offset": 0,
  "total": 14207,
  "markets": [
    {
      "id": "…uuid…",
      "venue": "polymarket",
      "venue_market_id": "0x…",
      "liquidity": 421300,
      "is_active": true,
      "raw_events": { /* parent event row */ }
    }
  ]
}

Prepare order

Builds a route plan and returns per-leg unsigned payloads. This is the first step of the two-step trade flow — it does not yet hit any venue.

POST /api/v1/orders/prepare read

Body parameters

NameTypeDescription
event_idrequireduuidUnified event id.
outcomerequired"YES" | "NO"Outcome side to trade.
siderequired"BUY" | "SELL"Direction.
size_usdrequirednumberNotional in USD. Must be positive.
max_priceoptnumberLimit price between 0 and 1.
time_in_forceopt"GTC" | "IOC" | "FOK"Default GTC.
route_strategyopt"best_price" | "split"Default best_price.
wallet_addressrequired0x…Polygon EOA that will sign Polymarket orders.
dflow_accountoptstringSolana address. Required if the route lands on Kalshi.
polymarket_maker_addressopt0x…Polymarket deposit-wallet (maker) address. Defaults to wallet_address.
client_refoptstringIdempotency key, scoped to your API key. 1–64 chars.

Example request

cURL
curl -X POST https://api.faro.markets/api/v1/orders/prepare \
  -H "Authorization: Bearer $FARO_KEY" \
  -H "content-type: application/json" \
  -d '{
    "event_id": "7c6a…",
    "outcome": "YES",
    "side": "BUY",
    "size_usd": 100,
    "max_price": 0.65,
    "wallet_address": "0xabc…",
    "dflow_account": "9aZ…",
    "client_ref": "trade-2026-05-09-001"
  }'

Response — 200

JSON
{
  "order": { "id": "…", "status": "prepared" },
  "plan": {
    "legs": [
      { "venue": "polymarket", "size_usd": 60, "expected_price": 0.63 },
      { "venue": "kalshi",     "size_usd": 40, "expected_price": 0.62 }
    ]
  },
  "legs": [
    { "id": "leg-1", "venue": "polymarket",
      "unsigned_payload": { "types": "…EIP-712…", "message": { } } },
    { "id": "leg-2", "venue": "kalshi",
      "unsigned_payload": { "transaction_b64": "AAAA…" } }
  ],
  "instructions": {
    "polymarket": "Sign each Polymarket leg's unsigned_payload as EIP-712…",
    "kalshi":     "Each Kalshi leg contains a base64 versioned Solana transaction…"
  }
}
Idempotency

If you pass client_ref and an order with the same (api_key, client_ref) already exists, the existing order + legs are returned as-is. Safe to retry.

Submit order

Submits each signed leg to its venue. Polymarket legs require an EIP-712 signature plus L2 HMAC headers; Kalshi legs require a base64 signed Solana transaction or an RPC signature if you submitted yourself.

POST /api/v1/orders/submit trade

Body — common

NameTypeDescription
order_idrequireduuidThe order.id returned by /orders/prepare.
legsrequiredarrayOne entry per leg you're submitting.

Body — Polymarket leg

NameTypeDescription
leg_idrequireduuidFrom the legs[] array of /orders/prepare.
venuerequired"polymarket"Discriminator.
signaturerequired0x…EIP-712 signature over unsigned_payload.message.
ownerrequired0x…Address of the signer.
l2_headersrequiredobjectCLOB L2 auth headers: POLY_API_KEY, POLY_PASSPHRASE, POLY_SIGNATURE, POLY_TIMESTAMP, POLY_ADDRESS.
order_typeopt"GTC" | "GTD" | "FOK" | "FAK"Defaults to the order's time_in_force.

Body — Kalshi leg

NameTypeDescription
leg_idrequireduuidFrom the legs[] array of /orders/prepare.
venuerequired"kalshi"Discriminator.
signed_tx_b64optstringBase64 of the signed versioned Solana transaction. Faro relays it to Solana RPC.
rpc_signatureoptstringIf you've already submitted on your own, the resulting Solana signature. Faro records it without re-broadcasting.

You must supply exactly one of signed_tx_b64 or rpc_signature for each Kalshi leg.

Response — 200

JSON
{
  "order_id": "…",
  "status": "submitted",   // or "partial" / "rejected"
  "legs": [
    { "leg_id": "leg-1", "venue": "polymarket",
      "ok": true, "venue_order_id": "0x…" },
    { "leg_id": "leg-2", "venue": "kalshi",
      "ok": true, "venue_order_id": "5Tx…SolSig" }
  ]
}

Get order

Returns the order plus all of its legs and their current per-venue status.

GET /api/v1/orders/{id} read

Order statuses

StatusMeaning
preparedPlan persisted, awaiting client signatures.
submittedAll legs accepted by their venue.
partialSome legs accepted, others rejected.
filledAll legs reported a fill.
cancelledAll legs successfully cancelled.
rejectedNo leg accepted.

Cancel order

Pass-through cancel for Polymarket legs. The client supplies their L2 HMAC headers per leg — Faro forwards them to the CLOB.

DELETE /api/v1/orders/{id} trade

Body

JSON
{
  "l2_headers_by_leg": {
    "<leg_id>": {
      "POLY_API_KEY":    "…",
      "POLY_PASSPHRASE": "…",
      "POLY_SIGNATURE":  "…",
      "POLY_TIMESTAMP":  "…",
      "POLY_ADDRESS":    "0x…"
    }
  }
}
Kalshi cancels are client-side

Kalshi swaps are RFQ-style and fill instantly when accepted, so there is no resting order to cancel. If you need to undo a Kalshi position, place an opposite trade.

Positions

Live read-through of positions per venue. Faro never stores balances — each call hits the upstream venue.

GET /api/v1/positions read

Query parameters

NameTypeDescription
walletrequired0x…Polygon address used to read Polymarket positions.
dflow_accountoptstringSolana address. DFlow does not yet expose a positions endpoint — read SPL token balances directly.

Response — 200

JSON
{
  "polymarket": [
    { "market": "0x…", "size": 120, "avg_price": 0.61 }
  ],
  "kalshi": {
    "note": "DFlow does not yet expose a positions endpoint — read SPL token balances on Solana for each outcome mint to derive positions."
  }
}

Balances

Live portfolio value per venue. Same model as positions — never cached, always read-through.

GET /api/v1/balances read

Query parameters

NameTypeDescription
walletrequired0x…Polygon address.

Response — 200

JSON
{
  "polymarket": { "portfolio_value_usd": 1842.36 },
  "kalshi": {
    "note": "DFlow does not expose balances directly. Read the Solana wallet's USDC balance via the standard Solana RPC."
  }
}

Issue API key

Mint a new API key. Plaintext is returned once. Stored hashed.

POST /api/v1/admin/keys admin secret

Body

NameTypeDescription
labelrequiredstring1–100 chars. Human-readable.
scopesopt("read" | "trade" | "admin")[]Default ["read"].
rate_limit_per_minoptinteger1–100,000. Default 60.
testoptbooleanIssue a test-mode key (faro_test_…) instead of faro_live_….

Response — 200

JSON
{
  "key": "faro_live_abc123…",
  "key_id": "…uuid…",
  "key_prefix": "faro_live_abc1",
  "label": "acme-corp",
  "scopes": ["read", "trade"],
  "rate_limit_per_min": 120,
  "created_at": "2026-05-09T12:34:56Z",
  "note": "The plaintext key above is shown ONCE. Store it now; it is unrecoverable."
}

List API keys

List all keys without their plaintext. Includes status, last-used-at, and rate limits.

GET /api/v1/admin/keys admin secret

Response — 200

JSON
{
  "keys": [
    {
      "id": "…uuid…",
      "key_prefix": "faro_live_abc1",
      "label": "acme-corp",
      "scopes": ["read", "trade"],
      "status": "active",
      "rate_limit_per_min": 120,
      "created_at": "2026-05-09T12:34:56Z",
      "last_used_at": "2026-05-09T18:01:22Z"
    }
  ]
}

Revoke API key

Soft-revokes a key by id. Subsequent requests using the key will return 401.

DELETE /api/v1/admin/keys/{id} admin secret

Response — 200

JSON
{ "id": "…uuid…", "status": "revoked" }

Sync events (cron)

Pulls the latest event/market snapshots from Polymarket and Kalshi (DFlow), upserts raw_events and raw_markets, and records a cron_runs entry. Runs hourly on Vercel.

POST /api/cron/sync-events cron secret

Both POST and GET are accepted (Vercel Cron sends GET). Authorization must be Bearer $CRON_SECRET.

Match markets (cron)

Daily LLM run that pairs markets from different venues that resolve identically. Only matches with confidence ≥ 0.85 are linked into unified_events; verdict, reasoning, and divergence risks are persisted to event_matches. Runs daily at 05:00 UTC.

POST /api/cron/match-markets cron secret