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.
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 https://api.faro.markets/api/v1/health
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.
Authorization: Bearer faro_live_<secret>
Scopes
| Scope | Grants |
|---|---|
read | All read endpoints (events, quotes, orderbook, markets, positions, balances). |
trade | Order preparation, submission, cancellation. Implies read. |
admin | Key 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 -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}'
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.
| Status | Code | Meaning |
|---|---|---|
400 | bad_request | Malformed input or invalid query parameters. |
401 | unauthorized | Missing, expired, or revoked API key. |
403 | forbidden | API key lacks the required scope. |
404 | not_found | Resource does not exist or is not visible to your key. |
429 | rate_limited | Per-minute key budget exceeded. Retry after the window. |
502 | upstream | An upstream venue (Polymarket or Kalshi) returned an error or was unreachable. |
500 | internal | Unhandled server error. |
{
"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.
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:
- Read each event's per-venue YES/NO bid/ask via
/events/:id/quotes. - Blend the venue probabilities (volume- or liquidity-weighted) into a single market-implied probability.
- Apply your house margin and convert to decimal, American, or fractional odds for your customer-facing UI.
- (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.
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 anoteif 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.
Response — 200
{
"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.
Query parameters
| Name | Type | Description |
|---|---|---|
limitopt | integer | Page size, 1–200. Default 50. |
offsetopt | integer | Pagination offset. Default 0. |
statusopt | "active" | "settled" | "all" | Defaults to active. |
end_beforeopt | ISO 8601 | Only events whose end-time is before the given timestamp. |
qopt | string | Full-text search across event title and description. |
Example request
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:
{
"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.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | Unified event id. |
Example request
curl https://api.faro.markets/api/v1/events/7c6a… \ -H "Authorization: Bearer $FARO_KEY"
Response — 200 (binary event)
{
"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[]:
{
"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.
Path parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | Unified event id. |
Response — 200
{
"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 }
}
]
}
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.
Query parameters
| Name | Type | Description |
|---|---|---|
venuerequired | "polymarket" | "kalshi" | Which venue's book to return. |
outcomeopt | "YES" | "NO" | Defaults to YES. |
Response — 200
{
"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.
Query parameters
| Name | Type | Description |
|---|---|---|
venuerequired | "polymarket" | "kalshi" | Which feed to return. |
limitopt | integer | Page size, 1–200. Default 50. |
offsetopt | integer | Pagination offset. Default 0. |
Response — 200
{
"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.
Target modes
Tell /orders/prepare what you want to trade by supplying one of the following:
| Mode | Fields | Use when |
|---|---|---|
| Binary event | event_id | You're trading a matched binary YES/NO event across all linked venues. |
| Categorical outcome | event_id + outcome_id | You're trading a single candidate inside a matched categorical event. |
| Single market | raw_market_id | You want to trade a specific venue listing directly (unmatched, single-venue, or one-off). |
Body parameters
| Name | Type | Description |
|---|---|---|
event_idopt | uuid | Unified event id. Pair with outcome_id for categorical events. |
outcome_idopt | uuid | Outcome scope under a categorical matched event. |
raw_market_idopt | uuid | Trade a specific venue market directly. Provide instead of event_id. |
outcomerequired | "YES" | "NO" | Outcome side to trade. |
siderequired | "BUY" | "SELL" | Direction. |
size_usdrequired | number | Notional in USD. Must be positive. |
max_priceopt | number | Limit price between 0 and 1. |
time_in_forceopt | "GTC" | "IOC" | "FOK" | Default GTC. |
route_strategyopt | "best_price" | "split" | Default best_price. |
wallet_addressopt | 0x… | EVM address that signs Polymarket orders. Required if the route includes a Polymarket leg. |
dflow_accountopt | string | Solana address that signs Kalshi swaps. Required if the route includes a Kalshi leg. |
polymarket_maker_addressopt | 0x… | Polymarket deposit-wallet (maker) address. Defaults to wallet_address. |
client_refopt | string | Idempotency key, scoped to your API key. 1–64 chars. |
Example request
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
{
"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."
}
}
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.
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.
Body — common
| Name | Type | Description |
|---|---|---|
order_idrequired | uuid | The order.id returned by /orders/prepare. |
legsrequired | array | One entry per leg you're submitting. |
Body — Polymarket leg
| Name | Type | Description |
|---|---|---|
leg_idrequired | uuid | From the legs[] array of /orders/prepare. |
venuerequired | "polymarket" | Discriminator. |
signaturerequired | 0x… | EIP-712 signature over unsigned_payload.message. |
ownerrequired | 0x… | Address of the signer. |
l2_headersrequired | object | CLOB 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
| Name | Type | Description |
|---|---|---|
leg_idrequired | uuid | From the legs[] array of /orders/prepare. |
venuerequired | "kalshi" | Discriminator. |
signed_tx_b64opt | string | Base64 of the signed versioned Solana transaction. Faro relays it to Solana RPC. |
rpc_signatureopt | string | If 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
{
"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.
Order statuses
| Status | Meaning |
|---|---|
prepared | Plan persisted, awaiting client signatures. |
submitted | All legs accepted by their venue. |
partial | Some legs accepted, others rejected. |
filled | All legs reported a fill. |
cancelled | All legs successfully cancelled. |
rejected | No 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.
Body
{
"l2_headers_by_leg": {
"<leg_id>": {
"POLY_API_KEY": "…",
"POLY_PASSPHRASE": "…",
"POLY_SIGNATURE": "…",
"POLY_TIMESTAMP": "…",
"POLY_ADDRESS": "0x…"
}
}
}
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.
Query parameters
| Name | Type | Description |
|---|---|---|
walletrequired | 0x… | Polygon address whose Polymarket positions you want. |
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
{
"polymarket": [
{ "market": "0x…", "size": 120, "avg_price": 0.61 }
]
}
Balances
Live portfolio value per venue. Same model as positions — never cached, always read-through.
Query parameters
| Name | Type | Description |
|---|---|---|
walletrequired | 0x… | Polygon address. |
Response — 200
{
"polymarket": { "portfolio_value_usd": 1842.36 }
}
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.
Body
| Name | Type | Description |
|---|---|---|
labelrequired | string | 1–100 chars. Human-readable. |
scopesopt | ("read" | "trade" | "admin")[] | Default ["read"]. |
rate_limit_per_minopt | integer | 1–100,000. Default 60. |
testopt | boolean | Issue a test-mode key (faro_test_…) instead of faro_live_…. |
Response — 200
{
"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.
Response — 200
{
"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.
Response — 200
{ "id": "…uuid…", "status": "revoked" }