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 — a daily LLM run pairs markets that resolve identically; only matches with
confidence ≥ 0.85are linked.
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 | Polymarket / Kalshi / DFlow returned an error. |
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 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.
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 (returns403without 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.
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
{
"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.
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
{
"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.
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.
Body parameters
| Name | Type | Description |
|---|---|---|
event_idrequired | uuid | Unified 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_addressrequired | 0x… | Polygon EOA that will sign Polymarket orders. |
dflow_accountopt | string | Solana address. Required if the route lands on Kalshi. |
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" },
"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…"
}
}
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 used to read Polymarket positions. |
dflow_accountopt | string | Solana address. DFlow does not yet expose a positions endpoint — read SPL token balances directly. |
Response — 200
{
"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.
Query parameters
| Name | Type | Description |
|---|---|---|
walletrequired | 0x… | Polygon address. |
Response — 200
{
"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.
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" }
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.
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.