Polyrank

Pay-Per-Request Market Data: No API Key, No Subscription

Build a TypeScript client that pays $0.01 in USDC per request over HTTP 402 with x402 — no API key, no signup. Full runnable code against Polyrank's agent API.

Polyrank12 min read

Every market-data API follows the same script: create an account, verify your email, generate a key, pick a plan, and pay a monthly bill whether your bot makes three thousand calls or three.

This tutorial does none of that. You'll build a TypeScript client that pays for prediction-market data one request at a time — $0.01 in USDC, settled on Base, negotiated over plain HTTP — using the x402 protocol. No signup. No API key to leak into a public repo. No subscription idling while your bot sleeps.

402 Payment Required has been reserved in the HTTP spec since 1997 and has sat there, unused, for almost thirty years. x402 finally makes it do its job: the server quotes a price inside the 402 response, your client pays it in-band, and the retry returns data.

The data you'll buy comes from Polyrank's agent API at api.polyrank.app/v1/agent/* (also served at api-agent.polyrank.app). Polyrank tracks roughly 2.8M Polymarket wallets across about 35M deduplicated trades and 3+ years of history, and ranks them by skill — calibration (Brier score), alpha-versus-mid, and risk-adjusted metrics — rather than raw P&L or win rate. The endpoint you'll hit returns the last 24 hours of smart-money signal events: which skill-ranked wallets moved, in which markets, on which side, at what price and size.

By the end of this post you will have: read a real HTTP 402 response and decoded its payment instructions field by field; built a paying client in six lines; run a ~40-line bot that fetches signals and prints a table; and bolted on the guardrails that keep an agent from ever spending more than you told it to.

What you need

  • Node 20+ — for native fetch and stable ESM.
  • A throwaway EVM private key — generated below. Never your main wallet. This key's entire job is to hold pocket change.
  • ~$1 of USDC on Base sent to that key's address. At $0.01 per signal pull, $1 buys about 100 calls.
  • Two packages: viem (accounts and signing) and @x402/fetch (the payment wrapper).
  • No ETH. This surprises people: the payment is an EIP-3009 signature, not a transaction you broadcast. The facilitator submits it on-chain and pays the gas. Your wallet needs USDC only.
node --version          # v20 or newer
mkdir x402-bot && cd x402-bot
npm init -y && npm pkg set type=module
npm install viem @x402/fetch
npm install -D tsx typescript

Generate the throwaway key:

// keygen.ts — run once, fund the address, keep the key in your env
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
 
const pk = generatePrivateKey();
console.log("address:    ", privateKeyToAccount(pk).address);
console.log("private key:", pk);
npx tsx keygen.ts
export AGENT_PRIVATE_KEY=0x...   # the key it printed

Send ~$1 of USDC on Base to the printed address (from an exchange or your main wallet), and you're funded.

Step 1 — ask without paying: the raw 402

Hit the endpoint with no auth, no key, no payment, and look at what comes back:

curl -i https://api.polyrank.app/v1/agent/signals/smart-money
HTTP/2 402 Payment Required
Content-Type: application/json
{
  "x402Version": 1,
  "error": "payment required: Recent smart-money signal events (last 24h)",
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "10000",
      "resource": "https://api.polyrank.app/v1/agent/signals/smart-money",
      "description": "Recent smart-money signal events (last 24h)",
      "mimeType": "application/json",
      "payTo": "0xEC89C6e7028b0e30E22eB3409d2F7c273EB20164",
      "maxTimeoutSeconds": 60,
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "extra": { "name": "USD Coin", "version": "2" }
    }
  ]
}

This is the entire price negotiation. Field by field:

  • x402Version: 1 — the protocol version. Your client checks this before doing anything else. (You may also notice a legacy "x402": "0.1" mirror field in the live response — it's there for pre-spec clients during a deprecation window; spec-compliant clients key on x402Version and ignore it.)
  • error — why you got a 402: you didn't attach a payment.
  • accepts[] — the list of payment options the server will take. There's one here; a client picks one it supports.
  • scheme: "exact" — pay an exact, fixed amount up front. (The spec leaves room for other schemes later — metered, streaming — but exact is what's live.)
  • network: "base" — the chain the payment settles on. Base mainnet.
  • maxAmountRequired: "10000" — the price, in atomic units of the asset. USDC has 6 decimals, so 10000 = $0.01. This is the number your guardrails check before signing anything.
  • resource — the URL you're buying. Bound into the payment so a quote for one endpoint can't be replayed against another.
  • description / mimeType — human-readable what-you-get, and the content type of the 200 response.
  • payTo — the recipient: Polyrank's treasury wallet. Verify it matches the address published at the free discovery endpoint, GET /x402/info.
  • maxTimeoutSeconds: 60 — the validity window. The authorization your client signs expires 60 seconds out, which tightly bounds replay.
  • asset — the ERC-20 contract you pay with: 0x833589…2913 is native USDC on Base.
  • extra: { name, version } — the asset's EIP-712 domain ("USD Coin", version "2"), which the client needs to construct the typed-data signature correctly.

Notice what's absent: no login URL, no key-exchange handshake, no account state. The 402 is a self-contained invoice.

Step 2 — the six-line client

Everything needed to pay that invoice automatically:

import { privateKeyToAccount } from "viem/accounts";
import { wrapFetchWithPayment } from "@x402/fetch";
 
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as `0x${string}`);
const fetchWithPay = wrapFetchWithPayment(fetch, account);
 
const res = await fetchWithPay("https://api.polyrank.app/v1/agent/signals/smart-money");
console.log(await res.json());

wrapFetchWithPayment returns a drop-in replacement for fetch. On a 402 it parses PaymentRequirements, signs the payment with your viem account, retries with the payment header, and hands you the 200. Your application code never sees the 402 at all.

Step 3 — a working bot in ~40 lines

The six-liner pays blindly, which you should never do in production. Here's the real version: a per-call price cap enforced before signing, a per-run budget ledger, and a table of the signals it bought.

// smart-money.ts — pay-per-request smart-money signals, no API key.
import { privateKeyToAccount } from "viem/accounts";
import { wrapFetchWithPayment } from "@x402/fetch";
 
const BASE_URL = "https://api.polyrank.app";
const MAX_PER_CALL = 10_000n;  // $0.01 in atomic USDC (6 decimals) — refuse to sign above this
const MAX_PER_RUN  = 100_000n; // $0.10 hard stop for the whole run
 
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as `0x${string}`);
// Third arg = maxValue: the wrapper rejects any 402 asking for more, BEFORE signing.
const fetchWithPay = wrapFetchWithPayment(fetch, account, MAX_PER_CALL);
 
let spent = 0n;
 
async function paidGet(path: string, price: bigint): Promise<unknown> {
  if (spent + price > MAX_PER_RUN) {
    throw new Error(`run budget exhausted: ${spent} atomic USDC already spent`);
  }
  const res = await fetchWithPay(`${BASE_URL}${path}`);
  if (!res.ok) throw new Error(`${path} -> HTTP ${res.status}`);
  spent += price;
  console.log(`settlement: ${res.headers.get("x-payment-response") ?? "(pending)"}`);
  return res.json();
}
 
type Signal = {
  trader: string; market: string; side: string;
  price: number; size: number; tags: string[];
};
 
const data = await paidGet("/v1/agent/signals/smart-money", 10_000n);
const signals = (Array.isArray(data) ? data : (data as { signals: Signal[] }).signals) as Signal[];
 
console.table(
  signals.slice(0, 15).map((s) => ({
    trader: s.trader.slice(0, 10) + "…",
    market: s.market.slice(0, 42),
    side: s.side,
    price: s.price,
    size: s.size,
    tags: s.tags.join(","),
  }))
);
 
console.log(`spent this run: $${(Number(spent) / 1e6).toFixed(2)} USDC`);
npx tsx smart-money.ts

First run, you'll see one paid call, fifteen rows of signal events, and a closing line like spent this run: $0.01 USDC. That dollar figure is not an estimate — it's a transfer you can now go find on-chain.

What just happened

Five things occurred between your fetchWithPay call and the table:

  1. Probe. The wrapper sent a normal GET. The server answered 402 with the PaymentRequirements you saw in Step 1.
  2. Budget check. The wrapper compared maxAmountRequired (10000) against the maxValue you configured. Over budget → it throws, and nothing is signed.
  3. Sign, locally. It built an EIP-3009 transferWithAuthorization for the USDC contract — from your throwaway address, to the treasury, value 10000, a validBefore about 60 seconds out, and a fresh random 32-byte nonce — and signed it as EIP-712 typed data with your viem account. No transaction was broadcast; this is just a signature, which is why your wallet needs zero ETH.
  4. Retry with payment. The same GET went out again with the signed authorization in the X-PAYMENT header (base64-encoded). The server passed it to a facilitator that verified the signature, the amount, the asset, the recipient, and that your address actually holds the funds — then ran the handler and returned 200 plus an X-PAYMENT-RESPONSE header carrying the settlement details, including the transaction hash. Settlement itself happens asynchronously, so the data path stays fast.
  5. Settle on-chain. The facilitator submitted your authorization to the USDC contract on Base, paying the gas itself.

Take the transaction hash from X-PAYMENT-RESPONSE and look it up on BaseScan. You'll find a USDC Transfer of exactly $0.01 from your throwaway address to the treasury — with the gas paid by the facilitator's address, not yours.

That's the whole trust model, and it matches how Polyrank treats its own analytics: every number traces to a public transaction. Your payment is auditable on Base; every signal you just bought traces to public Polygon transactions.

Guardrails: the runaway-agent problem, head on

The first question every developer asks about agent payments is the right one: what if my bot gets stuck in a retry loop and drains the wallet? Don't hand-wave it — bound it, in layers:

  1. Cap the call, before signing. The third argument to wrapFetchWithPayment is a ceiling in atomic units. Any 402 quoting more than that is rejected before a signature exists. The assertion maxAmountRequired <= budget must happen pre-signature — checking after is checking never. If you ever hand-roll a client, this check is non-negotiable.
  2. Cap the run. The MAX_PER_RUN ledger in the example is ten lines of arithmetic that turns "infinite retry loop" into "process exits at $0.10". Per-day caps work the same way with a persisted counter.
  3. Cap the wallet. This is the backstop that holds even if your code is wrong: the key holds $1, so the worst case of any bug, ever, is $1. Top it up deliberately; never point an agent at a wallet whose balance you wouldn't set on fire.
  4. Nonce hygiene. Every EIP-3009 authorization carries a single-use random 32-byte nonce, and @x402/fetch generates a fresh one per payment. Never reuse a nonce, and never re-send a previously signed X-PAYMENT header — a signed authorization is a bearer instrument for that exact amount until validBefore expires. Don't log payment headers; treat them like cash. The 60-second validity window bounds how long a leaked one matters, but the right amount of leaked authorizations is zero.
  5. Watch the spend. Print the run total (the example does), and alert if calls-per-hour exceeds what your polling schedule predicts. A bot that suddenly spends 10× is telling you about a bug for pennies.

Layered like this, the failure ceiling is the minimum of the four caps — and you chose every one of them.

Cost math

Prices from the free discovery endpoint (GET /x402/info — no payment needed):

EndpointWhat it returnsPrice$1 buys
GET /v1/agent/signals/smart-moneyLast-24h smart-money signal events$0.01100 calls
GET /v1/agent/trader/:proxyLifetime skill metrics for a wallet$0.005200 calls
GET /v1/agent/trader/:proxy/tagsBehavioral tags for a wallet$0.005200 calls
GET /v1/agent/market/:conditionIdSkill-weighted consensus vs. mid$0.01100 calls
GET /v1/agent/market/:conditionId/positioningTop-200 positioned wallets$0.0250 calls
GET /v1/agent/trader/:proxy/playbookTrader Playbook (categories, stake buckets, timing, co-traders)$0.01100 calls
GET /v1/agent/market/:conditionId/flow5-min flow buckets, trailing window$0.005200 calls
POST /v1/agent/rankings/customCustom composite ranking (your weights + filters)$0.0520 calls
GET /v1/agent/rankings/preset/:slugTop-N for a named skill preset$0.01100 calls

Concretely: polling smart-money signals every 15 minutes, around the clock, is 96 calls ≈ $0.96 a day. Enriching ten interesting traders with full skill metrics adds $0.05. When your bot stops calling, you stop paying — there is no idle subscription, no seat, no minimum.

What to build next

You now have the primitive: an HTTP client that can buy exactly the data it needs, when it needs it, with a spend ceiling you control.

  • The flagship: an autonomous smart-money watch agent — polling signals, enriching traders with GET /v1/agent/trader/:proxy, scoring markets with consensus-vs-mid, all pay-per-call. Tutorial coming soon.
  • The MCP route — Polyrank also ships an MCP server, so Claude and other MCP clients can query trader skill conversationally. See the MCP guide.
  • Webhooks — push instead of poll for fills and signal events, so you only pay for enrichment, not discovery.
  • Just browsing? The free public teaser API (and the site itself) need no wallet at all. See who's skilled — free lookup at polyrank.app.

Polyrank is an independent analytics product and is not affiliated with Polymarket. Everything here is read-only analytics — Polyrank never holds your funds or keys; the x402 wallet is yours and payments settle on a public chain. Nothing in this post is investment advice.

Newsletter

The weekly Smart-Money Report

Who moved, where the skilled money diverges from the crowd, and the calibration leader of the week. One email, no spam, unsubscribe anytime.