Smart Routing
How Pear scans every supported venue, picks the cheapest market for a given pear event, and hands the user a confirmable quote.
Smart Routing
A "pear event" (e.g. "Bitcoin price at 5pm EDT today") usually exists on multiple venues at the same time — Polymarket, Kalshi, and (soon) Manifold all list their own market with their own orderbook and their own fee schedule. Smart routing is the layer that fans out across every venue, prices the trade end-to-end, and ranks them so the user always gets the most shares per dollar.
This page explains the flow end-to-end. For raw schemas see the Cost and Execute reference pages.
The user-facing flow
The trade ticket walks through three states. Each maps to one API call.
| Step | Screen | API call |
|---|---|---|
| 1 | "Scanning venues…" while we fan out | GET /cost-rundown |
| 2 | "Best odds found" — venues sorted, cheapest highlighted | (renders the /cost-rundown response) |
| 3 | User taps Confirm (default best, or override to a non-best venue) | POST /execute |
The middle two screens never re-hit the API. One /cost-rundown call powers the entire venue-comparison UI; selection is local state.
GET /cost-rundown
The smart-routing read endpoint. Stateless and unauthenticated — no userId, no order created in Redis, safe to call on every keystroke as the user edits the trade size.
Rate-limited at the mobile_reads bucket (120/min) so the live preview can refresh as fast as the user types.
Request
GET /cost-rundown?pear_id=pear_btc_5pm_edt&amount_usd=50| Param | Required | Notes |
|---|---|---|
pear_id | yes | Canonical pear event id (pear_…). Whitespace gets normalised to _. |
amount_usd | one of | USD budget — the server probes the average venue price and converts to a contract count. |
size | one of | Explicit contract count. Use this if your UI thinks in shares, not dollars. Defaults to 100 when neither is set. |
Response
{
"pear": {
"id": "pear_btc_5pm_edt",
"title": "Bitcoin price at 5pm EDT today",
"slug": "btc-5pm-edt",
"source_type": "crypto"
},
"size": 75,
"amount_usd": 50,
"estimates": [
{
"venue": "polymarket",
"marketId": "0xabc…",
"marketTitle": "BTC ≥ $90k at 5pm EDT today",
"yesTokenId": "1234…",
"expectedAvgPrice": 0.6712,
"providerFee": 0.00,
"totalCost": 50.34,
"effectiveBps": 0,
"spread": 0.002,
"referencePrice": 0.67,
"priceImpact": 0.0012,
"liquidityAtBest": 240,
"fromLiveFeed": true
},
{
"venue": "kalshi",
"marketId": "KXBTC-26APR24-5PM",
"marketTitle": "BTC ≥ $90k at 5pm EDT today",
"expectedAvgPrice": 0.65,
"providerFee": 1.42,
"totalCost": 50.17,
"effectiveBps": 291,
"spread": 0.005,
"referencePrice": 0.645,
"priceImpact": 0.005,
"liquidityAtBest": 120,
"fromLiveFeed": false
}
],
"best": { /* same shape — the entry with the lowest totalCost */ },
"savingsVsWorst": 0.17,
"savingsVsWorstBps": 34
}The fields the venue-comparison UI cares about are:
| Field | Used for |
|---|---|
expectedAvgPrice | Per-row price label (e.g. "$0.67") |
totalCost | What the user actually pays — the canonical thing being compared |
providerFee, effectiveBps | Fee chip / breakdown drawer |
priceImpact, spread, liquidityAtBest | "Why is this venue worse?" tooltip |
best | Which row gets the green "Best" badge |
savingsVsWorst / savingsVsWorstBps | "You save $0.17 vs the worst venue" copy |
Pricing math
For each venue we walk the live orderbook to a fill, then layer the venue's published taker-fee schedule on top:
- Polymarket —
fee = size × feeRate × p × (1 − p), wherefeeRateis set per market category. Rates as of 2026-04:crypto 7.2%,sports 3%,finance 4%,politics 4%,mentions 4%,tech 4%,economics 5%,culture 5%,weather 5%,other 5%,geopolitics 0%. The exactfeeRateis read off the CLOB market info (fd.r) when available; we fall back to the category default. Sells are fee-free. - Kalshi (via DFlow) —
fee = ceil(takerScale × size × p × (1 − p), $0.01).takerScaleis tiered by 30-day outcome-token volume — Frost0.09is the conservative default we apply to every account.
totalCost = filledNotional + providerFee. Pear-side fees are billed separately downstream and do not influence routing — the cheapest venue here is the cheapest venue you'll actually pay.
Best-venue selection
best is just the entry with the lowest totalCost. Ties (rare, since fees differ) are broken by the array order, which itself is the order the cost engine returned the venues.
The user is always free to override. The middle screen below shows Polymarket selected because it's best; the right screen shows the user picking Kalshi anyway and the UI flagging the gap.
POST /execute
Once the user taps Confirm, the trade goes through /execute — that's the only stateful, user-authenticated part of the flow.
POST /execute
Content-Type: application/json
Authorization: Bearer <privy-access-token>
{
"pearId": "pear_btc_5pm_edt",
"venue": "kalshi", // optional — defaults to the routed best
"marketId": "KXBTC-26APR24-5PM", // optional — defaults to the routed best for the venue
"size": 75,
"side": "yes",
"live": true // false to dry-run without hitting the venue
}The handler:
- Re-runs the cost engine against current orderbooks (the user could have sat on the screen for a while).
- Picks the venue from the body, falling back to the freshly routed best.
- Calls the matching adapter — Polymarket CLOB on Polygon, or Kalshi via DFlow on Solana.
- Persists the order + fill to Redis and Supabase, then publishes onto the
orders:{userId}WebSocket topic.
If the user didn't override the venue, the result is identical to "trust the router". If they did override (right-hand screen in the mockup), the override wins; we just don't refuse on price grounds — that's the user's call.
POST /route exists too and is conceptually the same as the cost-rundown call followed by an order reservation, but for new clients we recommend /cost-rundown → user picks → /execute. The reservation step in /route mostly exists for legacy clients that want a single committed orderId before the trade fires.
Why splitting routing and execution matters
Keeping the smart-routing read separate from the execution write gives us three properties the UI flow above depends on:
- Anonymous quotes. The whole "scan → present → confirm" loop works without a logged-in user. Marketing pages, share-links and embedded widgets can all show live, accurate routing without an auth round-trip.
- Polling-friendly. Because no Redis state is created, the ticket can re-quote on every size change without leaking orphaned
QUOTEDorders. - Honest comparisons. Routing is purely a function of orderbook + venue fee schedule, never a function of who's asking. Two users on the same trade always see the same
best.
Multi-venue caveat
The cost engine currently knows about Polymarket and Kalshi. The mockup includes Manifold as a third row — that's intentional foreshadowing, not yet wired up. Once a Manifold adapter ships it'll appear automatically in the estimates[] array; clients should already render whatever the API returns rather than hard-coding two columns.
Failure modes
| Status | Meaning | UI hint |
|---|---|---|
400 | pear_id missing or malformed | "Couldn't find that market" |
404 | Pear event resolved but no venue is currently quoting | "No venues live for this market right now" |
503 | Cost engine module failed to load | "Pricing temporarily unavailable" |
Empty-book venues (no asks for a buy, no bids for a sell) are silently dropped from estimates[] rather than returned with a null price — the fewer rows the UI has to special-case the better.