Skip to content

Instantly share code, notes, and snippets.

@brennancheung
Last active February 9, 2026 22:03
Show Gist options
  • Select an option

  • Save brennancheung/f126f0208cf710fef882f6dd9b2d45e3 to your computer and use it in GitHub Desktop.

Select an option

Save brennancheung/f126f0208cf710fef882f6dd9b2d45e3 to your computer and use it in GitHub Desktop.

Polymarket Integration: Investigation & Status Report

Context

When clicking Polymarket odds in the Value Dashboard to place an order, the request fails with a 403 Forbidden from Cloudflare. This prompted a full investigation of the click-to-trade flow, existing Polymarket infrastructure, and backend architecture.


1. Click-to-Trade Flow (Full Trace)

Step 1: UI Click Handler — ConsolidatedMoneylines.tsx

The consolidated moneylines table renders Kalshi and Polymarket odds side by side. Each odds cell has a click handler:

  • Kalshi cells → call handleTrade(teamSide)
  • Polymarket cells → call handlePolyTrade(teamSide)

These are correctly separated — the system does NOT accidentally route Poly clicks through Kalshi.

Step 2: TradeClickRequest Creation

handleTrade() (Kalshi) — creates a request with venue: 'kalshi', marketTicker, side, priceCents, contracts.

handlePolyTrade() (Polymarket) — creates a request with venue: 'polymarket' plus Polymarket-specific fields: tokenId, conditionId, tickSize, negRisk. It resolves the opponent's market for "NO" semantics (team win = NO on opponent).

Step 3: Modal — PlaceOrderModal.tsx

The TradeClickRequest is stored in state, which opens PlaceOrderModal. The modal:

  • Shows a venue badge (blue for Kalshi, purple for Polymarket)
  • Lets users adjust contract quantity
  • Shows expiry options for Kalshi only (Polymarket always uses GTC)
  • Converts the request into a PlaceOrderRequest

Step 4: Order Submission — ValueDashboardView.tsx

handleSubmitTrade() branches on tradeReq.venue:

if venue === 'kalshi'      → onPlaceKalshiOrder(...)
if venue === 'polymarket'  → onPlacePolymarketOrder(...)

This routing is correct.

Step 5: Execution — App.tsx

Kalshi path: handlePlaceKalshiDashboardOrder()api.placeOrder() → relay → Kalshi API ✅

Polymarket path: handlePlacePolymarketDashboardOrder()placePolyOrder()ClobClient.postOrder() → direct POST to clob.polymarket.com/order403 Forbidden

Complete Call Chain

User clicks odds cell in ConsolidatedMoneylines
  → handlePolyTrade() creates TradeClickRequest { venue: 'polymarket', tokenId, ... }
    → onTradeClick(req) callback
      → ValueDashboardView.setTradeReq(req) — opens PlaceOrderModal
        → User clicks "Place Order"
          → handleSubmitTrade(submission)
            → onPlacePolymarketOrder(args)
              → App.tsx handlePlacePolymarketDashboardOrder()
                → placePolyOrder({ client, tokenId, action: 'buy', ... })
                  → ClobClient.createOrder() — signs locally with ethers.Wallet ✅
                  → ClobClient.postOrder() — POST to clob.polymarket.com/order ❌ 403

2. The 403 Problem

Root Cause

@polymarket/clob-client is a Node.js SDK. When used in the browser:

  1. Browser adds Origin and Referer headers to the outgoing request
  2. Cloudflare on clob.polymarket.com sees the cross-origin request
  3. Cloudflare returns 403 Forbidden

This is the exact same problem Kalshi has, which is why we built the relay server.

Evidence (from DevTools)

Request URL:    https://clob.polymarket.com/order
Request Method: POST
Status Code:    403 Forbidden
Server:         cloudflare
Referrer Policy: strict-origin-when-cross-origin

Call stack:

onClick                    @ PlaceOrderModal.tsx:265
(anonymous)                @ ValueDashboardView.tsx:388
(anonymous)                @ App.tsx:1192
await in placePolyOrder    @ client.ts:93
await in postOrder         → clob.polymarket.com → 403

Scope of Impact

This affects all Polymarket CLOB API calls from the browser, not just order placement:

  • createOrDeriveApiKey() — credential derivation
  • postOrder() — order placement
  • getOpenOrders() — order refresh
  • cancelOrder() / cancelAll() — order cancellation (not yet implemented)

3. Existing Polymarket Infrastructure

What's Built

Capability Status File
Order placement (buy/sell, limit, GTC) ✅ Code exists, blocked by 403 lib/polymarket/client.tsplacePolyOrder()
Client creation + wallet signing ✅ Works (client-side) lib/polymarket/client.tscreatePolyTradingClient()
API credential derivation ✅ Code exists, likely blocked by 403 lib/polymarket/client.tscreateOrDerivePolyApiCreds()
User order/trade WebSocket stream ✅ Works (WebSocket, not HTTP) lib/polymarket/userStream.ts
Market book WebSocket stream ✅ Works (WebSocket, not HTTP) lib/polymarket/marketStream.ts
Market data via Gamma API ✅ Works lib/polymarket/gamma.ts
Open orders tracking ⚠️ Partial, best-effort refresh App.tsx post-placement call
Order cancellation ❌ Not implemented
Config persistence (localStorage) ✅ Works client.tsloadPolyUserConfig() / savePolyUserConfig()

Key Technical Details

  • Package: @polymarket/clob-client v5.2.1
  • Signing: ethers v5.8.0 Wallet — private key stays in browser
  • Order types: GTC only, supports postOnly (maker) and taker modes
  • Price: Cents (1-99), normalized to 0.0001-0.9999 decimal
  • Chain: Polygon (chain ID 137)

What's NOT Built

  1. Order cancellation@polymarket/clob-client supports cancelOrder() and cancelAll() but we haven't implemented wrappers or UI for them. Kalshi has both.
  2. Sell via click-to-tradeaction: 'buy' is hardcoded in ValueDashboardView.tsx:391. No way to sell Polymarket positions from click-to-trade.
  3. Robust order refresh — The post-placement getOpenOrders() call silently swallows errors in a catch {} block.

4. Backend / Relay Status

Current State: Kalshi-Only

The relay server (apps/relay/) is a generic HTTP + WebSocket forwarder, but production mode restricts it to Kalshi:

// apps/relay/src/httpRelay.ts:80-86
if (config.nodeEnv === 'production') {
  const url = new URL(request.url);
  const kalshiHost = new URL(config.kalshiBaseUrl).hostname;
  if (url.hostname !== kalshiHost) {
    throw new ValidationError(`URL must be from ${kalshiHost} in production`);
  }
}

Architecture Comparison

Kalshi (works):
  Browser → signs request (RSA-PSS) → relayHttp envelope → Relay (8787) → Kalshi API
  
Polymarket (broken):
  Browser → signs order (ethers Wallet) → ClobClient POST → clob.polymarket.com → 403

Polymarket WebSockets (works):
  Browser → wss://ws-subscriptions-clob.polymarket.com → market data / user events

WebSockets work because they handle CORS differently than HTTP POST requests.


5. Proposed Fix

Option A: Transparent Proxy Route on Relay (Recommended)

Add a /poly/* route to the relay that transparently proxies to clob.polymarket.com/*.

New flow:
  Browser → ClobClient(HOST=relay/poly) → Relay /poly/* → clob.polymarket.com ✅

Implementation:

  1. Add Express route: app.all('/poly/*', ...) that strips /poly prefix and forwards to clob.polymarket.com
  2. Change HOST in lib/polymarket/client.ts from https://clob.polymarket.com to the relay URL
  3. Wallet signing still happens client-side — private key never leaves the browser

Why this is best: Fixes ALL Polymarket HTTP calls (orders, cancellations, credential derivation, open orders) in one shot. No changes needed to the ClobClient usage patterns.

Option B: Split Sign vs. Post (Alternative)

Use ClobClient.createOrder() for local signing only, then POST the signed payload through our existing relay envelope format.

Why this is worse: Only fixes order posting. Every other CLOB API call (createOrDeriveApiKey, getOpenOrders, cancelOrder) would need its own manual implementation.


6. Additional Work Items

After fixing the 403:

  1. Implement order cancellation — Add cancelPolyOrder() and cancelAllPolyOrders() wrappers using the ClobClient methods. Add UI controls.
  2. Add sell support to click-to-trade — Allow selling Polymarket positions (right-click, modifier-click, or toggle).
  3. Improve order refresh — Make post-placement order refresh reliable instead of best-effort silent catch.
  4. Test credential derivationcreateOrDerivePolyApiCreds() also calls the CLOB API and would have hit the same 403. Verify it works through the relay.

Key Files Reference

File Purpose
apps/dashboard/src/components/nba-value-dashboard/ConsolidatedMoneylines.tsx Click handlers for odds cells (handleTrade, handlePolyTrade)
apps/dashboard/src/components/nba-value-dashboard/PlaceOrderModal.tsx Order confirmation modal
apps/dashboard/src/components/pages/ValueDashboardView.tsx Routes orders to correct venue handler
apps/dashboard/src/App.tsx handlePlaceKalshiDashboardOrder, handlePlacePolymarketDashboardOrder
apps/dashboard/src/lib/polymarket/client.ts placePolyOrder(), createPolyTradingClient(), config persistence
apps/dashboard/src/lib/polymarket/userStream.ts WebSocket user order/trade events
apps/dashboard/src/lib/polymarket/marketStream.ts WebSocket market book data
apps/dashboard/src/lib/polymarket/gamma.ts Gamma API for market metadata
apps/relay/src/httpRelay.ts HTTP relay (currently Kalshi-only)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment