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 — markets that resolve under identical conditions are paired across venues, so each unified event reflects every venue trading the same question.

Not sure where to start? Skip to use cases.

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.
502upstreamAn upstream venue (Polymarket or Kalshi) returned an error or was unreachable.
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 sees private keys. Clients hold their own wallets on each chain:

  • A Polygon wallet with USDC.e to fill Polymarket legs.
  • A Solana wallet with USDC to fill Kalshi legs.

Faro doesn't bridge USDC across chains. If your wallet only holds USDC on one side, bridge before calling /orders/prepare.

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 with your own wallet, then call POST /orders/submit to relay them.

Use cases

Faro exposes one API; how you use it depends on what you're building. The four patterns below cover most integrations we see.

Fixed-odds bookmaking

For sportsbooks, betting houses, and gaming operators who want to quote fixed odds to retail bettors. Use the Data API (/events, /events/:id, /events/:id/quotes) to pull cross-venue probability streams in real time:

  1. Read each event's per-venue YES/NO bid/ask via /events/:id/quotes.
  2. Blend the venue probabilities (volume- or liquidity-weighted) into a single market-implied probability.
  3. Apply your house margin and convert to decimal, American, or fractional odds for your customer-facing UI.
  4. (Optional) Lay off net exposure on the Trading API when one side runs hot.

Why this works: each unified event aggregates the deepest venues for that question, so the implied probability is more accurate than any single venue's odds. Faro is not in the betting flow — you keep your customer relationship and your float, and you only pay for the data you consume.

Typical bookmaker flow

Poll /events?status=active on a 30–60s cadence for board population, then drive per-event WebSocket-style updates from /events/:id/quotes behind your odds engine. Settle off the same unified event when it resolves.

Aggregated trading apps

For partners building broker-style trading apps, wallets, and prediction-market frontends. Use the Data API to render markets and the Trading API (/orders/prepare, /orders/submit) to route orders to the venue with the best fill — or split across venues. Your users sign locally with their own wallets; Faro never touches keys.

Research & analytics

For quants, market makers, and journalists studying prediction markets. The unified event feed gives you matched markets across venues; /markets/raw exposes the full per-venue universe (matched and unmatched) for divergence research, custom matching, or cross-venue alpha.

Resolution data

Pull settled events to power resolution dashboards, payout reconciliation, or post-event analytics. Pass ?status=settled to /events and you'll get only events that have already resolved on at least one venue.

Concepts

Unified events

A unified event is a single cross-venue concept — one row that points at every venue trading the same underlying question. The event's title, description, resolution_criteria, and end_date come from the venue with the highest liquidity at match time (the primary_venue). The matches[] array carries every linked venue listing, with that venue's live raw_market snapshot.

You can also work with venue-native rows directly via /markets/raw when you need the unmatched universe.

Binary vs categorical events

A binary event has a single YES/NO question — for example "Will X happen by Dec 31?". For binary events, event.matches[] is populated and event.outcomes[] is empty.

A categorical event has multiple candidates — for example "Who wins the 2028 Democratic nomination?". Each candidate becomes one entry in event.outcomes[], with its own matches[] array. The top-level event.matches[] is empty for categorical events; trade scope is per-outcome.

To trade a categorical event, pass both event_id and outcome_id to /orders/prepare.

Routing model

When you call /orders/prepare, Faro fetches the live order book on every venue linked to the event, walks the merged book in price priority, and returns:

  • plan.legs[] — the recommended per-venue split that fills your size at the best blended price.
  • routes[] — alternatives you can pick instead: recommended, plus single-venue routes (polymarket_only, kalshi_only) when distinct.
  • venues_considered[] — every venue we evaluated, with best price and available size, plus a note if you don't have the right wallet to execute on that venue.

The execution path is filtered to venues whose chain matches a wallet you provided. Cross-chain funding is your responsibility — Faro doesn't bridge.


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

Each entry is a full unified event DTO — same shape as GET /events/{id}, abbreviated here for readability:

JSON
{
  "events": [
    {
      "id": "7c6a…",
      "title": "Will X happen by Dec 31?",
      "description": "…",
      "end_date": "2026-12-31T23:59:00Z",
      "primary_venue": "polymarket",
      "is_active": true,
      "matches":  [/* venue listings — see Get event */],
      "outcomes": []  // non-empty for categorical events
    }
  ],
  "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 (binary event)

JSON
{
  "event": {
    "id": "7c6a…",
    "title": "Will X happen by Dec 31?",
    "description": "…drawn from primary venue…",
    "resolution_criteria": "…",
    "end_date": "2026-12-31T23:59:00Z",
    "primary_venue": "polymarket",
    "is_active": true,
    "matches": [
      {
        "venue": "polymarket",
        "is_primary": true,
        "raw_event":  { "id": "…", "title": "…", "end_date": "…", "liquidity": 421300 },
        "raw_market": { "id": "…", "venue_market_id": "0x…", "yes_price": 0.63 }
      },
      {
        "venue": "kalshi",
        "is_primary": false,
        "raw_event":  { "id": "…", "title": "…", "end_date": "…", "liquidity": 87200 },
        "raw_market": { "id": "…", "venue_market_id": "X-Y-Z", "yes_price": 0.62 }
      }
    ],
    "outcomes": []
  }
}

Response — 200 (categorical event)

For categorical events the top-level matches[] is empty and matches live under each entry of outcomes[]:

JSON
{
  "event": {
    "id": "…",
    "title": "2028 Democratic nominee",
    "matches": [],
    "outcomes": [
      {
        "id": "out-1",
        "label": "Gavin Newsom",
        "ordering": 0,
        "is_active": true,
        "matches": [
          { "venue": "polymarket", "raw_market": { "yes_price": 0.18 } },
          { "venue": "kalshi",     "raw_market": { "yes_price": 0.17 } }
        ]
      },
      { "id": "out-2", "label": "Gretchen Whitmer", "matches": [/* … */] }
    ]
  }
}

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

Target modes

Tell /orders/prepare what you want to trade by supplying one of the following:

ModeFieldsUse when
Binary eventevent_idYou're trading a matched binary YES/NO event across all linked venues.
Categorical outcomeevent_id + outcome_idYou're trading a single candidate inside a matched categorical event.
Single marketraw_market_idYou want to trade a specific venue listing directly (unmatched, single-venue, or one-off).

Body parameters

NameTypeDescription
event_idoptuuidUnified event id. Pair with outcome_id for categorical events.
outcome_idoptuuidOutcome scope under a categorical matched event.
raw_market_idoptuuidTrade a specific venue market directly. Provide instead of 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_addressopt0x…EVM address that signs Polymarket orders. Required if the route includes a Polymarket leg.
dflow_accountoptstringSolana address that signs Kalshi swaps. Required if the route includes a Kalshi leg.
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" },

  // The recommended split that the legs[] below were built for
  "plan": {
    "legs": [
      { "venue": "polymarket", "size_usd": 60, "expected_price": 0.63 },
      { "venue": "kalshi",     "size_usd": 40, "expected_price": 0.62 }
    ],
    "filled_usd": 100, "unfilled_usd": 0, "vwap": 0.626
  },

  // Per-venue snapshot — every venue we evaluated, even if you can't sign for it
  "venues_considered": [
    { "venue": "polymarket", "best_price": 0.63, "available_usd": 5400, "chosen": true,  "note": null },
    { "venue": "kalshi",     "best_price": 0.62, "available_usd": 2100, "chosen": true,  "note": null }
  ],

  // Alternative routes you can pick instead of the recommended split
  "routes": [
    { "name": "recommended",     "label": "Best blended price", "vwap": 0.626, "filled_usd": 100 },
    { "name": "polymarket_only", "label": "Polymarket only",     "vwap": 0.635, "filled_usd": 100 },
    { "name": "kalshi_only",     "label": "Kalshi only",         "vwap": 0.628, "filled_usd": 100 }
  ],

  "legs": [
    { "id": "leg-1", "venue": "polymarket",
      "unsigned_payload": { "domain": { }, "types": { }, "message": { } } },
    { "id": "leg-2", "venue": "kalshi",
      "unsigned_payload": { "transaction_b64": "AAAA…" } }
  ],

  "instructions": {
    "polymarket":          "Sign each Polymarket leg's unsigned_payload as EIP-712 typed-data with your EVM wallet, then POST {leg_id, venue:'polymarket', signature, l2_headers, owner} to /api/v1/orders/submit.",
    "kalshi":              "Each Kalshi leg's unsigned_payload contains a base64 versioned Solana transaction at .transaction_b64. Sign with your Solana keypair, then either submit yourself OR POST {leg_id, venue:'kalshi', signed_tx_b64} to /api/v1/orders/submit.",
    "cross_chain_funding": "Faro does not bridge USDC across chains. If your wallet only holds USDC on one chain, bridge it yourself before calling /orders/prepare."
  }
}
Connect at least one wallet

If you provide neither wallet_address nor dflow_account, the request returns 400 with venues_considered populated so you can show the user which wallet is missing.

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 whose Polymarket positions you want.
Kalshi positions

Faro does not aggregate Kalshi positions through this endpoint. To derive Kalshi positions for a Solana wallet, read its SPL token balances per outcome mint via your own Solana RPC.

Response — 200

JSON
{
  "polymarket": [
    { "market": "0x…", "size": 120, "avg_price": 0.61 }
  ]
}

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 }
}
Solana balances

For Solana balances, query your wallet's USDC SPL balance via your own 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" }