Pear Exchange

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.

StepScreenAPI call
1"Scanning venues…" while we fan outGET /cost-rundown
2"Best odds found" — venues sorted, cheapest highlighted(renders the /cost-rundown response)
3User 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
ParamRequiredNotes
pear_idyesCanonical pear event id (pear_…). Whitespace gets normalised to _.
amount_usdone ofUSD budget — the server probes the average venue price and converts to a contract count.
sizeone ofExplicit 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:

FieldUsed for
expectedAvgPricePer-row price label (e.g. "$0.67")
totalCostWhat the user actually pays — the canonical thing being compared
providerFee, effectiveBpsFee chip / breakdown drawer
priceImpact, spread, liquidityAtBest"Why is this venue worse?" tooltip
bestWhich 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:

  • Polymarketfee = size × feeRate × p × (1 − p), where feeRate is 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 exact feeRate is 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). takerScale is tiered by 30-day outcome-token volume — Frost 0.09 is 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:

  1. Re-runs the cost engine against current orderbooks (the user could have sat on the screen for a while).
  2. Picks the venue from the body, falling back to the freshly routed best.
  3. Calls the matching adapter — Polymarket CLOB on Polygon, or Kalshi via DFlow on Solana.
  4. 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 QUOTED orders.
  • 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

StatusMeaningUI hint
400pear_id missing or malformed"Couldn't find that market"
404Pear event resolved but no venue is currently quoting"No venues live for this market right now"
503Cost 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.

On this page