# Authentication
Source: https://docs.polytape.io/authentication
API keys and OAuth for the PolyTape agent plane — minting, header usage, key limits per plan, and what a key can and cannot do.
Two credential families. Both are **header-only**, and both draw on **one
per-user budget**.
## API keys
```http theme={null}
Authorization: Bearer td_live_...
```
Mint a key in the dashboard: **AI Agents & API → Patch Bay**.
Dashboard → **AI Agents & API**. The Patch Bay is the rear panel where your
ports live.
Give the key a label — one per agent or machine, so you can unplug one
without disturbing the others.
The plaintext `td_live_` token exists **exactly once**, in the mint response.
We store only its hash and the last four characters for display. It cannot be
shown again.
If you lose a key, you cannot recover it. Revoke it and mint a new one.
### Rules that bite
* **Header only.** A key is never accepted in a query string. A key in a URL is a
key in an access log.
* **Revocation is instant.** A revoked key fails on its very next call — there is
no cache to wait out. Plan downgrades bite the next request too.
* **Keys are cables, not quota.** Every key you own spends from the same call
budget. A second key gives you a second plug, not a second allowance.
### Ports per plan
| Plan | API keys |
| ------------------------- | -------- |
| Basic Intelligence (free) | 1 |
| Pro Intelligence | 3 |
| Max Intelligence | 6 |
The cap counts **non-revoked** keys. Minting past it returns `402` with a message
naming your plan's limit — unplug a key or upgrade.
## OAuth (MCP)
Some MCP clients — the claude.ai custom connector, Claude Code, Cursor,
opencode — can connect over OAuth instead of a pasted key. You sign in with your
PolyTape account and consent; the client handles tokens from there. No key
material is ever pasted or stored in a config file.
The connect panel in the dashboard shows the exact per-client steps and whether
OAuth is live for your client yet. Where it is not, the key + `Authorization:
Bearer` path always works.
OAuth sessions and API keys spend from the **same** per-user budget. Connecting
a different way does not get you a separate allowance.
## What a key can and cannot do
Read everything on this API — traders, markets, fills, tapes, the popular
shelf — and open live agent streams for wallets. All read-only.
Mint or manage keys (a dashboard action, never an API-key action), write
anything, or place a trade. V1 is read-only by construction.
## Auth failures
| HTTP | error | meaning |
| ---- | ------------------------ | ----------------------------------------------------------------------------- |
| 401 | `missing_credentials` | no `Authorization` header |
| 401 | `invalid_api_key` | unknown or revoked key |
| 429 | `too_many_auth_failures` | too many bad keys from your IP — fix the credential, then wait out the window |
401s carry a `WWW-Authenticate: Bearer` header. See [Errors](/errors) for the
full table.
# Reading the numbers
Source: https://docs.polytape.io/concepts/pnl
Three P/L voices live on this API. Which one to quote, how to label it, and the caveats that keep a verdict honest.
An agent sees three profit-and-loss figures on this API. They answer different
questions. Quoting the wrong one is the most common way to be confidently wrong
about a trader.
## The one to quote
On the trader card, the `polymarket` block carries **Polymarket's own model**:
realized **plus** unrealized, over the trader's **full account history
including the pre-V2 era**. This is the number the user sees on
polymarket.com. Quote it as the trader's profit.
```json theme={null}
"polymarket": {
"pnl": 61240.0,
"pnl_realized": 55010.0,
"pnl_unrealized": 6230.0,
"portfolio_value": 12400.0,
"peak": 71000.0,
"low": -3400.0,
"max_drawdown": 42000.0,
"as_of": 1783200000
}
```
If Polymarket's upstream is unreachable the block degrades to `{"status":
"pm_offline"}` and the rest of the card still serves. **Only then** does the card
carry the archive's own `career_pnl_usd` / `pnl_marked_usd` as a labeled
fallback. On a healthy response those fields are absent by design — there is one
headline P/L, not two competing ones.
## The other two voices
Per-market `pnl_usd` (and the card's `career_pnl_usd` twin, served only when
Polymarket is offline) is **realized cash flow only** — reconstructed and
chain-audited over the **V2-contract era (roughly April 2026 onward)**.
Use it for per-market analysis and anything that must trace to the chain.
An active trader with a large open book reads **more negative** here than the
headline, because money deployed counts as spent until the market resolves.
That is not a discrepancy; it is a different question.
Realized plus open holdings valued at the last on-chain trade. PolyTape's own
estimate of where a standing position sits. Useful, honest, **not the
headline**.
## Labeling a result
`pnl_usd` is a **result**. "Won $X" / "lost $X" is fair.
`pnl_usd` is **cash flow**, not a result. Quote `pnl_marked_usd`, say
"marked at $X" — never "won $X".
## The caveats that keep you honest
A market's `pnl_usd` includes mint, split, and redemption legs that never
appear as buy or sell fills. A −$42.3k market can show buy $2k / sell \$41.7k
and simply not reconcile from the fills alone.
**Quote `pnl_usd`; do not reconstruct P/L from visible fills.** And `roi` is
meaningless for split-sellers — pnl ÷ buy-notional explodes when the "entry"
was a mint rather than a buy. Do not quote it on sell-dominated flow.
The card's counts, winrate, and volume cover the V2 era until the v1 backfill
lands. The headline P/L covers the full account.
State that once, then quote the headline with confidence.
A small class — dozens of accounts — run NegRisk-conversion arbitrage, where a
conversion moves balances without a trade. Their per-token accounting is
distorted.
If a card shows large volume with near-zero or nonsensical realized P/L,
suspect a conversion-arb bot rather than an edge.
Not one of these three figures is a live account balance. Do not present any
of them as one.
## avg\_entry\_price — two different stats, one name
This trips up more agents than anything else on the surface. The field appears in
two places and means two different things.
`card.avg_entry_price` is the average cost **per token** across all buys —
dollars spent ÷ tokens bought, **both outcome sides**, in the token's own
frame.
That makes it the trader's **break-even winrate**. Compare it against
`winrate` to price the edge:
* 63% winrate at a 0.47 average entry → real selection skill.
* 95% winrate at a 0.95 average entry → none.
It is **token-weighted**, so cheap-token volume drags it well below the price
the trader typically clicks. A trader who enters at 90¢ or better nearly every
time can still read 0.627 here, and that is correct arithmetic, not a bug.
**Do not describe it as "the price they usually pay."**
`summary.avg_entry_price` is the **UP-space VWAP** of that trader's buys
**within one market**. A different frame and a different question.
The per-token `sides[].avg_entry` inside the same response is in each token's
*own* frame — what they actually paid for that token.
`avg_entry_price`, `avg_win_usd`, and `avg_loss_usd` are **absent or null until
the career-stats rollup has populated a trader**. Treat missing as
"unavailable" — never as zero.
`avg_win_usd` / `avg_loss_usd` are per-resolved-market **averages, not
medians**, and one blowup skews them. Read them beside `get_top_markets`, not
alone.
## A verdict that traces
Every claim should trace to a number a tool returned. If the record looks
incomplete, say so rather than guessing. What that reads like:
```text theme={null}
ACROSS 5,154 MARKETS: $10.2M volume, +$1.1M (Polymarket) — a grinder in the black.
ENTRY: buys around even odds (VWAP ~0.54), not chasing extremes.
SIZE: steady ~$19 clips with occasional conviction lumps up to ~$48k.
TIMING: near-zero early, ~57% of capital in the last tenth — a late piler.
EDGE: size + timing pays on the winners (this one +$234k at 65% ROI).
LEAK: winrate 41% — the late-pile conviction loses more markets than it wins.
VERDICT: fade the aggregate, but the late conviction lumps are worth watching.
```
Chart labeling follows the same doctrine — see [Plotting](/guides/plotting).
# Token thrift
Source: https://docs.polytape.io/concepts/token-thrift
Payloads on this API vary by three orders of magnitude. Two rules save most of it.
Measured on real traders, the cheapest call on this API is about 70 tokens and
the most expensive is about 35,000. Knowing which is which is most of the skill.
## What things cost
| Call | Typical size | Notes |
| ----------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------ |
| `whoami` / `get_trader_card` / `get_top_markets` / `get_market_summary` | \~70–2k | Cheap. Lean on these. |
| **`get_market_events`** (sampled, \~300 pts) | **\~2.5–3k REST / \~5–6k MCP** | One call = the whole chart. Ask `points=120` for chat. |
| `get_pnl_series` | \~1–2k | The equity curve in one call. |
| **`list_trader_markets`** (full 200-row page) | **\~16k REST / \~35k MCP** | **Always pass `limit`.** |
| `get_recent_fills` (full page) | \~10–15k | Cap with `limit`. |
## The two rules
`get_market_summary` and `get_top_markets` already contain sizing, timing,
entry pricing, and per-market P/L — as computed numbers, with no arrays.
They answer "how did they trade this?" outright.
Reach for `get_market_events` when you need to **chart** a market or confirm a
specific sequence. Not before.
MCP returns every result **twice** — a readable text copy and a structured
copy — so paged tools cost roughly **double** their REST equivalent.
A default `list_trader_markets` page is \~35k tokens. `limit: 20` turns that
into \~2k. The same applies to `get_recent_fills` (`limit`) and
`get_market_events` (`points`).
The single biggest waste on the whole surface is an unbounded
`list_trader_markets` or `get_recent_fills` over MCP. One careless call can
cost more tokens than an entire well-run trader crack.
## Workflow costs
| Workflow | Calls | Tokens (thrifty) |
| ---------------------------------- | ----- | ----------------- |
| Compare two traders (cards only) | 2 | \~180 |
| Read one market (summary only) | 1–2 | \~250–2k |
| Crack a trader (full) | 6–10 | \~3–5k |
| Plot a market (sampled + template) | 1–2 | \~3–6k |
| Monitor poll (quiet / busy) | 1 | \~0.3k / \~10–15k |
## Reach for the right primitive
`list_trader_markets` is **recency-ordered only**, by design. A 40,000-market
bot has no "biggest win" on page one, and paging 200 times to find it costs a
fortune and still misses.
`get_top_markets` returns `top_wins`, `top_losses`, `most_traded`, and
`top_volume` from **one scan**. That is the extremes primitive.
Keep `n` modest on mega-makers — a 180,000-market account sits near the
heavy-query timeout, and a narrow `n` is both cheaper and likelier to return.
`get_market_events` returns a decimated price `series` **plus the trader's
complete fills** in a single response. No cursor, no tick flood.
`raw=1` returns the verbatim tick pages instead, where the same second
repeats about **36 times** — a 5-minute market becomes 8,000 rows across 4
pages. It exists as an export path. You almost never want it for a chart.
`get_recent_fills` with a `since` watermark holds no live slot and costs one
request per poll. Record the last fill's `ts` and pass it back next time so
you only pull what is new.
Poll on a **minutes** cadence. Freshness is bounded by the archive pipeline
anyway, and a tight loop burns a Basic day in under an hour.
Cursors are opaque keyset tokens, forward-only. Never hand-craft or mutate
one.
A malformed cursor is a clean `400 bad_cursor`. A **well-formed forged**
cursor silently returns page 1 again — safe depth-wise, but you re-read and
re-pay for data you already have.
## Validate before you fire
Application errors on real routes **burn a call**. A typo'd `condition_id` is a
`404` that costs a Basic user 1/50th of their day. `429` rejections do not burn
a call, so the rate limiter is free — but your own mistakes are not.
# Errors
Source: https://docs.polytape.io/errors
Every error code the PolyTape agent plane returns, what it means, and whether to retry.
Errors are structured JSON:
```json theme={null}
{ "error": "", "message": "", "retry_after": }
```
`retry_after` is present on `429`s. On MCP the same failures arrive as readable
tool results (`isError: true` with text), never as opaque protocol errors.
## The table
| HTTP | error | meaning |
| ---- | ------------------------ | ---------------------------------------------------------------------------------- |
| 400 | `bad_cursor` | malformed cursor — only ever page with a `next_cursor` you were handed |
| 400 | `bad_limit` | `limit` must be ≥ 1 |
| 400 | `invalid_wallet` | a `0x…` string of the wrong hex length. It never falls through to name resolution. |
| 400 | `unknown_event_type` | an unrecognized value in `types` |
| 401 | `missing_credentials` | no `Authorization` header (`WWW-Authenticate: Bearer`) |
| 401 | `invalid_api_key` | unknown or revoked key |
| 403 | `depth_gated` | the market is past your history window (Basic Intelligence = 5 markets per trader) |
| 403 | `wallet_quota` | Basic Intelligence's 3-distinct-wallets-per-UTC-day cap |
| 404 | `not_found` | no such market for this trader |
| 404 | `unknown_trader` | no resolvable wallet, URL, or name |
| 404 | `tape_not_found` | no such tape in your library |
| 429 | `rate_limited` | token bucket or per-minute ceiling hit |
| 429 | `daily_limit` | **the paywall** — Basic Intelligence's daily call quota is spent |
| 429 | `monthly_limit` | **the paywall** — Pro / Max monthly call quota is spent |
| 429 | `busy` | heavy-read admission lane full |
| 429 | `too_many_auth_failures` | too many bad keys from your IP |
| 503 | `agent_api_disabled` | the agent surface is temporarily off |
| 503 | `archive_offline` | the archive is temporarily unreachable |
| 503 | `pm_offline` | Polymarket's upstream is temporarily unreachable |
| 504 | `query_timeout` | the query was too broad (or `top-markets` `n` too large) — narrow and retry |
## How to react
`rate_limited` · `busy` · `archive_offline` · `agent_api_disabled`
Honor `Retry-After` (seconds) where present. These clear in seconds. A
well-behaved agent waits rather than failing the turn.
`pm_offline` is a special case: the trader card and pnl-series **degrade
gracefully** rather than erroring. The card serves with
`"polymarket": {"status": "pm_offline"}` and carries the archive's realized
P/L as a labeled fallback. Use it, say which voice you are quoting, and move
on.
`daily_limit` · `monthly_limit`
The `message` is **written to be relayed to your human**. It names the
upgrade and links `https://polytape.io/pricing`, and it is plan-aware (Pro is
told about Max).
Surface it and stop. `retry_after` on a daily cap points hours out and on a
monthly cap points weeks out.
`depth_gated` · `wallet_quota`
Not outages. Retrying changes nothing — the answer will be identical until
the plan changes or the day rolls over.
```json theme={null}
HTTP 403
{ "error": "wallet_quota",
"message": "…names the plan, the upgrade, and the reset…",
"used": 3,
"limit": 3,
"resets_at": "2026-07-29T00:00:00Z" }
```
`resets_at` is an ISO 8601 UTC string (next midnight UTC) — the one timestamp
on this API that is not epoch seconds. There is **no `retry_after`** here.
Treat `depth_gated` as an upgrade prompt, or as a not-found if you would
rather not interrupt.
`bad_cursor` · `bad_limit` · `invalid_wallet` · `unknown_event_type` ·
`not_found` · `unknown_trader` · `tape_not_found`
Do not retry the same request. Fix the argument.
These **burn a call** (the counter charges served work), so validate before
you fire — a typo'd `condition_id` costs a Basic user 1/50th of their day.
`missing_credentials` · `invalid_api_key` · `too_many_auth_failures`
Revocation is instant, so a key that worked a minute ago can be dead now. Fix
the credential, then wait out the auth-failure window before retrying.
`query_timeout`
Almost always a mega-maker (a 100k+ market account) plus a wide `n` on
`top-markets`. Narrow `n` and retry — a small `n` is both cheaper and
likelier to return.
## What burns a call
Every successful GET or tool call. **And** application errors on real routes —
`400`, `403`, `404`, even `304`.
All `429` rejections. MCP protocol handshakes: connect, `tools/list`,
`prompts/list`, fetching a prompt.
## MCP error text
The same failures, as tool results:
| Text | Underlying |
| ----------------------------------------------------- | ------------------------------------------------- |
| `rate limit: retry in Ns` | `rate_limited` or `busy` |
| A message naming an upgrade and `polytape.io/pricing` | `daily_limit`, `monthly_limit`, or `wallet_quota` |
| `this market is outside your history window` | `depth_gated` |
| `no such market for this trader` | `not_found` |
| `no such tape in your library` | `tape_not_found` |
| `the trader archive is not available…` | `archive_offline` |
# Plotting
Source: https://docs.polytape.io/guides/plotting
Charts your user can read — the grammar, the validated palette, the honest labels, and the ready-made templates you pour data into.
PolyTape's data draws two charts well: **a market's price line with a trader's
fills on it**, and **a trader's career equity curve**. This is the spec — the
grammar, the palette, and the honest labels — plus templates you pour data into.
Everything here is deterministic. The price line an agent draws is the same line
the PolyTape web app draws; the fidelity law is shared, byte for byte. You are
not inventing a look, you are reproducing one.
## Which chart, and how to reach it
| You want to show… | Draw | Template |
| --------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------- |
| A market's price path + where the trader got in/out | the **market chart** | `chart-market-*.html` / `chart-market.py` |
| A trader's cumulative P/L over their career | the **equity curve** | `chart-equity.html` / `chart-equity.py` |
| A distribution (sizes, hold times, hourly activity) | a table or a plain bar chart off `get_market_summary` | — the summary already has the numbers |
**Two market-chart flavors, routed by where the chart will live:**
**The SVG template** (`chart-market-svg.html`). Self-contained: no CDN, no
external fonts, no fetches, \~30KB. Renders inside a chat artifact or a strict
CSP.
Reach it with `get_chart_template(kind="market", format="html")`.
**The tape template** (`chart-market-tape.html`). TradingView Lightweight
Charts for kinetic pan/zoom over the whole tape down to 1-second buckets.
Fetch it from
[`/skill/reference/templates/`](https://polytape.io/skill/reference/templates/chart-market-tape.html),
or `get_chart_template(kind="market-tape")`.
Both draw the identical grammar and palette. **Pick by capability, not
preference.**
***
## The market chart — grammar
One market. Time on x (the viewer's local time), UP-space probability on y (0–1,
shown as ¢). Five marks, in this order:
The drawn value is the **mid of the traded band**: `mid = (lo + hi) / 2` for
each point `[ts, last, lo, hi]`. `last` is the bucket's final real trade;
`lo` / `hi` are the band edges.
**Every one of those is a real traded level** — the server never emits a
computed price, so the line never fabricates a number. Draw it stepped or
straight; do not interpolate across gaps with a solid line.
A faint blue fill between `lo` and `hi`. It shows where trading actually
happened; the line rides its middle.
Shape encodes side; **color encodes which token they traded**; marker area is
proportional to `usd`, so a conviction slam reads bigger than a \$2 clip.
**Fills RIDE THE LINE.** A marker's y is the *line's value at the fill's
moment*, not the raw fill price. At fine zoom the line ≈ the trade price, so
riding it is the truth; at coarse zoom the line is the only honest y a bucket
can show, and a price-true marker would float and read broken. **The exact
fill price lives in the hover docket, never in the plotted position.** No
stems.
**×N pile badges.** When 3 or more fills share an x-slot, label the cluster
`×N` instead of overprinting; merge colliding badges so counts never overlap.
If the market resolved, walk a dashed **amber `#ffb84d`** segment from the
last point to the winner level (token-1 win → 1.00, else 0.00) and pin it with
a label.
A dashed bridge, never a solid line — the price didn't trade there, it
settled there.
Frame the band's actual extent (plus the winner level if resolved), padded
\~18%, clamped to \[−0.02, 1.02].
Do **not** force 0–100¢ on a market that lived between 40¢ and 60¢ — you'd
flatten the whole story.
Header chips carry the context: trader, RESOLVED·winner / OPEN, the P/L (labeled
per the doctrine below), fill count, and a **token legend** — a colored swatch
next to each token's name.
***
## The palette
The tones are locked and ported verbatim from the web app's `outcomeTone` rule.
Don't eyeball them and don't recolor.
The CRT tube the chart sits in is **always dark** (`#101410`) — in both day and
night; the theme switch reskins only the room around the glass. So every mark is
validated against one constant surface.
| Role | Color | Notes |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------- |
| Price line + band | `#3d7dff` Polymarket blue | one series; passes lightness + contrast |
| **UP family** (`up`/`yes`/`over`/`true`/`win`/`higher`/`above`/`long`) | `#46ff8e` phosphor green | token 1 on most binaries |
| **DOWN family** (`down`/`no`/`under`/`false`/`lower`/`below`/`short`) | `#ff5a3c` loss red | token 2 on most binaries |
| **Named outcomes** (a candidate, a team) | djb2 hash → `#ffb000` · `#3fd8d0` · `#b98cff` · `#5aa9ff` · `#ff79c6` · `#8f9bff` | deterministic per name |
| Resolution / drawdown pin | `#ffb84d` amber | dashed walk + dot |
**Colorblind safety, measured.** Against the dark tube, the green↔red pair that
appears on nearly every market separates at **ΔE 29.0** for deuteranopia — far
above the 12 target. The two-sided coloring is colorblind-safe on its own.
The named-outcome palette is a deterministic hue assignment, not a CVD-optimized
ramp: two named tokens can hash to a close pair (worst adjacent blue↔violet is
ΔE 3.1). **That is why the labeled token legend is mandatory and the hover docket
names the token in words** — identity is never carried by color alone.
The phosphor tones sit brighter than a reflective-surface lightness band by
design (they glow on a black tube); they clear the contrast-vs-surface floor,
which is what governs legibility here.
***
## Getting the data
**The default call gives you the whole chart in one shot.** `get_market_events`
(REST `GET /traders/{trader}/markets/{condition_id}`) returns a **sampled**
response — no cursor, no tick flood:
```json theme={null}
{ "market": { "condition_id": "0x…", "title": "…", "outcome": "Up",
"outcome2": "Down", "resolved": true, "winner": "Up",
"pnl_usd": 81.08, "open": false },
"trader": { "wallet": "0x…", "name": "GoingInsolvent" },
"series": { "from_ts": …, "to_ts": …, "fidelity": 1,
"points": [[ts, last, lo, hi], …] },
"fills": [ { "ts": …, "side": "BUY", "price": 0.48, "token_side": 2,
"outcome": "Down", "size_tokens": 5.0, "usd": 2.6 }, … ] }
```
* `points` are pre-decimated to your `points=` target — **default 300 = web
parity, clamp 20–600**, snapped to the fidelity ladder. Ask for `points=120`
for a chat-sized chart.
* The default window is **title-anchored**: a "2:25–2:30PM ET" market opens at
the title's start. Pass `from_ts` / `to_ts` to zoom.
* `fills` is the trader's **complete** fill set, always present. Each carries
`token_side` (1|2) **and** `outcome` (the token's name) — you no longer derive
the side.
* Draw `(lo + hi) / 2`. This one response fills the SVG template directly.
When you want continuous zoom from a 53-day arc down to a single second, fetch
the whole change-compressed tape in one gzip response from
`GET /traders/{trader}/markets/{condition_id}/tape` —
`{market, trader, tape: [[ts, last, lo, hi], …]}`, per traded second,
uncapped.
The Iran market is 27,066 tape points (\~150KB gzipped): fine for a file, too
big for a chat artifact. The tape template does all bucketing and the band-mid
walk client-side.
The old keyset tick pages (`ticks` + `cursor`) survive behind `?raw=1`. You
almost never want this for a chart: raw ticks duplicate about **36×** (the
same second repeats), so a 5-minute market is 8,000 rows across 4 pages. The
sampled default exists precisely to spare you this.
**The `limit=1` fills-only trick is dead.** Fills used to arrive in full on
the first page regardless of `limit`, so agents passed `limit=1` to get fills
without paying for ticks. The sampled default **always** includes the complete
fills — just call it plainly.
**For "what happened at the end?", pass a `from_ts` / `to_ts` window near the
close.** There is no descending or `until` tick paging; it is a forward keyset
cursor, so a tight end-window is the endgame view.
***
## Token frame — the one rule that keeps you honest
**The price line is in UP space** (token 1's probability, 0–1). `mid`, `last`,
`lo`, `hi`, and a fill's `price` are all UP-normalized. That is the correct,
single frame for the *line* — one market, one price path.
But a trader buys a **token**, and the price they *paid* is in that token's own
frame: for a token-1 buy it's `price`; for a token-2 buy it's `1 − price`
(= `usd / size_tokens`).
The chart handles this by **coloring the marker by token** (`token_side` →
`outcome` name → tone) while keeping every marker on the one UP line.
This is why the coloring is load-bearing, not cosmetic. Without it, a two-sided
scalper who buys UP at 0.48 and DOWN at 0.55 seconds apart looks like one
person averaging a single position — **the single most common misread on the
whole surface.** Color it, and the two-sided structure is instantly legible.
Name the tokens in the title and legend so "0.94" reads as "UP at 94¢", not an
ambiguous number.
***
## Labeling a chart's P/L
The [three-voice doctrine](/concepts/pnl) applies to every chart you emit:
* A **resolved** market's `pnl_usd` is a *result* — "won $X" / "lost $X".
* An **open** market's `pnl_usd` is **cash flow**, not a result. Money deployed
counts as spent. Label it `CASH FLOW`, and quote `pnl_marked_usd` for the
position's standing ("marked at $X") — never "won $X".
* **Split/merge legs are invisible to fills.** A market's `pnl_usd` includes
mint, split, and redemption legs that never appear as buy/sell fills — so a
−$42.3k market can show buy $2k / sell \$41.7k and not reconcile. Don't
reconstruct P/L from visible fills; quote `pnl_usd`. And **`roi` is meaningless
for split-sellers** — don't quote it on sell-dominated flow.
***
## The equity curve + drawdown
`get_pnl_series` (REST `GET /traders/{trader}/pnl-series`) returns Polymarket's
own cumulative-P/L series — the same model as the headline:
```json theme={null}
{ "trader": {…}, "window": "all", "model": "polymarket_profile",
"series": [[ts, pnl_usd], …], "peak": …, "low": …,
"max_drawdown": …, "as_of": … }
```
`window` ∈ `24h | 7d | 30d | ytd | 1y | all` (default `all`). Pour it into
`chart-equity.html` / `chart-equity.py`.
* **Equity** — the cumulative line with a **zero baseline**; fill green above
break-even, red below. The final value's sign colors the end dot.
* **Drawdown** — derived in one line: **`dd[i] = p[i] − running_max`** (0 at
every new high, negative while underwater). `max_drawdown` arrives precomputed;
the deepest trough gets the amber pin. This is the honest "how bad did it get?"
view and it needs **no extra call** — the template computes it from `series`.
***
## Whales — don't page, ask the right primitive
A 40,001-market bot has no "biggest win" on page one. **Never page the markets
list 200× to find extremes** — the list is recency-ordered only, by design.
* **`get_top_markets(trader, n)`** returns four ranked slices from one scan:
`top_wins`, `top_losses` (realized P/L, **resolved markets only** — honestly
labeled), `most_traded` (by fills), `top_volume`. Keep `n` modest on
mega-makers; a 180,000-market account is near the heavy-query timeout.
* The card's **career averages** answer "is this winrate any good?".
`avg_entry_price` is the average cost per token across all buys — the
**break-even winrate**. `avg_win_usd` / `avg_loss_usd` (served negative) are
**averages, not medians**, so one blowup skews them: read them alongside
`top_losses` rather than alone.
***
## Using the templates
Each HTML template has a single placeholder — `const DATA = "__DATA__";` — that
appears exactly once. Replace it with your JSON object and render:
```js theme={null}
const tpl = fs.readFileSync("chart-market-svg.html", "utf8");
const data = /* the sampled response from get_market_events */;
fs.writeFileSync("chart.html", tpl.replace('"__DATA__"', JSON.stringify(data)));
```
The templates **normalize defensively** — they accept the canonical shape
(`series.points`, `size_tokens`, `pnl_usd`, `trader.name`) and tolerate the
fixture-style shape (`points`, `size`, `pnl`, `trader_name`) — so pouring a raw
tool response in usually just works.
| Template | Consumes | Theme | Notes |
| ------------------------ | --------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------- |
| `chart-market-svg.html` | the sampled response (`series`/`points` + `fills`); or `slabs:[…]` for baked zoom | night default, day switch | self-contained; chat-safe |
| `chart-market-tape.html` | `{market, trader_name, fills, tape:[[ts,last,lo,hi]]}` | night default, `window.__setTheme("day")` | TradingView; continuous 1s zoom |
| `chart-equity.html` | `{series:[[t,p]], peak, low, max_drawdown}` | night default, day switch | PNL / DRAWDOWN toggle |
| `chart-market.py` | same as SVG (argv JSON path) | `THEME` constant | matplotlib → PNG |
| `chart-equity.py` | same as equity (argv JSON path) | `THEME` constant | matplotlib → PNG, 2-panel |
**Deep-link and test hooks (HTML):** `window.__setRange(fromTs, toTs)` (tape),
`window.__setDom(fromTs, toTs)` (SVG), `window.__setMode("pnl"|"drawdown")`
(equity), `window.__setTheme("day"|"night")`, and `window.__ready === true` once
painted.
**Zoom on the SVG flavor = baked slabs, not live fetch.** One sampled call =
one slab. To zoom deep in a chat artifact, bake 2–3 windows (the full arc plus
the drama stretch) with `from_ts` / `to_ts` and pass them as
`slabs: [{fidelity, from_ts, to_ts, points}, …]`; the view swaps to the finest
slab covering the range.
**Never wire a chat chart to live-fetch — the file would carry your API key.**
**Reaching a template through the connector.** If your client can't fetch web
files, `get_chart_template(kind, format, theme)` returns the file text through
the MCP tool itself. It costs 1 request — a normal tool call.
***
## Minimal end-to-end
```text theme={null}
get_market_summary(trader, market) # the shape + the window + resolved?
get_market_events(trader, market, points=300) # one call: series + all fills + header
→ pour the response into chart-market-svg.html
(or fetch chart-market-tape.html for a file)
→ verdict: quote pnl_usd (resolved) with the token named; if two-sided, say so —
the coloring already shows it.
```
That is a two-sided scalp and a 53-day blowup, each in **one events call** after
the summary — house-grade, correctly token-colored, honestly labeled.
# Recipes
Source: https://docs.polytape.io/guides/recipes
Five worked end-to-end flows with measured call and token costs — crack a trader, compare two, read a market, monitor a wallet, plot the tape.
Five flows, measured. Tool names are the MCP form; the REST path in parentheses
is identical data. Costs assume you follow [token thrift](/concepts/token-thrift)
— a small `limit` on lists, summaries before raw events.
***
## Crack a trader
**Goal:** decode how a trader actually trades and deliver a verdict.
**Cost:** \~6–10 calls, \~3–5k tokens (add \~1 call and \~3–6k for a chart).
\~70 tokens. Call it once at the start of a run so you know what you have.
*(`GET /traders/{trader}`)* — the career line. **Quote `polymarket.pnl` as
the headline profit.**
Note anything lopsided: big volume with thin P/L, a winrate far from 50%, or a
career `avg_entry_price` far from the winrate. That last one is the
break-even winrate — *"63% winrate at a 0.47 average entry is an edge; 72% at
0.63 is too."* \~90 tokens.
*(`…/top-markets`)* — the biggest wins, worst losses, most-traded, and
top-volume markets in **one scan**. This is where the technique shows, and it
works on a 40,000-market whale that the recency-ordered markets list cannot
sort. Pick the 2–3 most revealing. \~1–2k tokens.
Only need *recent* activity instead? `list_trader_markets(trader, limit=25)`
*(`…/markets?limit=25`)*. Pass the `limit`.
*(`…/markets/{cid}/summary`)* — read `avg_entry_price` against the outcome,
the size distribution (`size_p50_usd` vs `size_max_usd` — steady clips or
conviction lumps?), `hold_seconds`, the first/last-tenth timing shares (early
opener or late sniper?), and the per-token `sides` block (two-sided?).
\~250 tokens each.
On that **one** market — read the sampled price path against the fills. Each
fill carries `token_side` and `outcome`, so two-sided flow is explicit.
\~2–3k tokens.
See [Plot the tape](#plot-the-tape) below. This is the closing step in the
`crack_this_trader` prompt.
Then write the verdict. A real example — a trader's biggest market read straight
off one summary (*buy 361k / sell 179k, avg entry 0.54, size p50 $19 vs max
$48.5k, 57% of money in the final tenth, +\$234k / 65% ROI*):
```text theme={null}
ACROSS 5,154 MARKETS: $10.2M volume, +$1.1M (Polymarket) — a grinder in the black.
ENTRY: buys around even odds (VWAP ~0.54), not chasing extremes.
SIZE: steady ~$19 clips with occasional conviction lumps up to ~$48k.
TIMING: near-zero early, ~57% of capital in the last tenth — a late piler.
EDGE: size + timing pays on the winners (this one +$234k at 65% ROI).
LEAK: winrate 41% — the late-pile conviction loses more markets than it wins.
VERDICT: fade the aggregate, but the late conviction lumps are worth watching.
```
**The `crack_this_trader` prompt does exactly this** — invoke it instead of
scripting the steps by hand. Fetching it is free.
Every claim should trace to a number a tool returned. If the record looks
incomplete — counts, winrate, and volume cover the V2 era until the v1 backfill —
say so rather than guessing.
***
## Compare two traders
**Goal:** which of two is better, and do they overlap?
**Cost:** \~4–6 calls, \~1–2k tokens.
Line up the headlines (`polymarket.pnl`), winrate, `avg_entry_price`, volume,
and tenure. This alone answers "who's better" at the portfolio level for about
180 tokens.
Compare **shape**: one big winner carrying the record versus many small edges.
And check whether their biggest markets overlap — the same `condition_id`s
mean correlated books.
To see who entered better, or earlier.
Card-only comparison is the cheapest high-signal move on the whole API. Two
\~90-token calls settle most "who's better" questions.
On **Basic Intelligence**, comparing two traders spends **two of your three
daily wallet lookups**. Repeat reads of either wallet that day are free — so do
the whole comparison in one session rather than coming back tomorrow.
***
## Read one market's tape
**Goal:** what happened in a market — the price path, and where a trader got in
and out?
**Cost:** \~2–3 calls, \~1–3k tokens.
The shape first: entry VWAP, the per-token `sides` block, hold time, outcome,
P/L. Usually the whole answer.
*(`GET /traders/{trader}/markets/{condition_id}`)* — the **sampled**
UP-normalized price `series` plus the trader's `fills`, each with `token_side`
and `outcome`, so you can place entries on the curve *and* say which token.
Pass `from_ts` / `to_ts` to zoom a window; `points` (default 300) to size the
series.
See below.
`series.points` are `[ts, last, lo, hi]` in UP space (0–1); draw `(lo + hi) / 2`.
A fill's `side` is BUY/SELL, `usd` its notional, and `token_side` / `outcome`
name the token.
For **"what happened at the end?"**, pass a tight `from_ts` / `to_ts` window
near the market's close. There is no descending or `until` tick paging — it is a
forward keyset cursor — so a narrow end-window *is* the endgame view.
***
## Monitor a wallet
**Goal:** follow a trader's new activity without holding a live stream.
**Cost:** 1 call per poll, \~0.3–15k tokens depending on activity.
`get_recent_fills(trader, since=, limit=100)`
*(`…/fills?since=…&limit=100`)*.
Each fill carries `condition_id`, `title`, `side`, `price`, `size_tokens`,
`usd`, `token_side`, and `outcome` — enough to fingerprint a trader (a
two-sided BTC scalper versus a directional bettor) in **one call**.
Save the last fill's `ts`. Next poll, pass it back as `since` — or page via
`next_cursor` — so you only pull what is new.
Not a tight loop. Freshness is bounded by the archive pipeline anyway
(minutes), and a slow poll respects both the rate limits and your call quota.
A Basic Intelligence day of 50 calls burns fast under a tight loop.
This is the cheapest way to "watch" a trader: it holds **no live slot**, and each
poll is one request. A quiet trader costs a few hundred tokens per poll; cap busy
ones with `limit`.
***
## Plot the tape
**Goal:** a price chart of one market with the trader's fills in context.
**Cost:** 1 sampled call (\~2.5–3k tokens REST / \~5–6k MCP) plus an optional
template fetch (1 call).
It tells you whether the market resolved, and its window.
`get_market_events(trader, market, points=300)`
*(`GET /traders/{trader}/markets/{condition_id}`)* returns `market`,
`trader`, a decimated `series`, and the trader's **complete** `fills`. No
cursor, no tick flood. Ask for `points=120` for a chat-sized chart.
*Deep interactive zoom in a file?* Fetch the whole change-compressed **tape**
from `GET /traders/{trader}/markets/{condition_id}/tape` — one gzip response,
`[[ts, last, lo, hi]]` per traded second — and use the tape template.
* **Chat / connector context →** the self-contained SVG:
`get_chart_template(kind="market", format="html")`.
* **File / browser context →** the TradingView flavor:
[`chart-market-tape.html`](https://polytape.io/skill/reference/templates/chart-market-tape.html).
Replace the single `"__DATA__"` placeholder with your JSON. The templates draw
the house grammar for you.
A **resolved** market's `pnl_usd` is a result. An **open** market's is **cash
flow** — quote `pnl_marked_usd`, say "marked", never "won \$X". Split/merge legs
live inside `pnl_usd` and never show as fills, so do not reconstruct P/L from
the visible buys and sells, and do not quote `roi` on a split-seller. Name the
token so a "0.94" reads as "UP at 94¢".
Full spec, palette, and the templates: [Plotting](/guides/plotting).
***
## Cost cheat sheet
| Workflow | Calls | Tokens (thrifty) |
| ---------------------------------- | ----- | ----------------- |
| Compare two traders (cards only) | 2 | \~180 |
| Read one market (summary only) | 1–2 | \~250–2k |
| Crack a trader (full) | 6–10 | \~3–5k |
| Plot a market (sampled + template) | 1–2 | \~3–6k |
| Monitor poll (quiet / busy) | 1 | \~0.3k / \~10–15k |
The single biggest waste is an unbounded `list_trader_markets` or
`get_recent_fills` over MCP (\~35k / \~15k tokens). Always pass `limit`.
# PolyTape for agents
Source: https://docs.polytape.io/index
The Polymarket trader-intelligence layer for you and your AI agent. Every trader's full on-chain history, over MCP or REST.
**The Polymarket trader-intelligence layer for you and your AI agent.**
Point an agent at a Polymarket wallet and it can read that trader's whole
on-chain career: every market they touched, how they sized, when they got in,
what it cost them, and whether the record reads like skill or luck.
Two surfaces, one key, one budget:
`https://api.polytape.io/mcp` — 13 tools and a `crack_this_trader` prompt for
Claude Code, claude.ai, Cursor, opencode, and any MCP client.
`https://api.polytape.io/api/agent/v1` — every read is a GET. For scripts,
cron jobs, and anything that curls JSON.
Three ways in — the claude.ai connector, an MCP client, or a raw key — in under
five minutes.
## What you can ask it
| Question | Where to start |
| ---------------------------------------------------------- | ------------------------------------------------------------- |
| "How does this trader actually trade? What's their edge?" | [Crack a trader](/guides/recipes#crack-a-trader) |
| "Which of these two is better — do they overlap?" | [Compare traders](/guides/recipes#compare-two-traders) |
| "What happened in this market, and where did they get in?" | [Read a market's tape](/guides/recipes#read-one-markets-tape) |
| "Chart it / show me their equity curve" | [Plotting](/guides/plotting) |
| "Has this wallet done anything new?" | [Monitor a wallet](/guides/recipes#monitor-a-wallet) |
## What's underneath
The archive is our own indexed copy of both Polymarket V2 exchanges — roughly
**500M ticks across 1.7M markets**, keyed by trader. Every price point the API
returns is a level that actually traded; the server never emits a computed
price. The line an agent draws is byte-for-byte the line the PolyTape web app
draws.
## Read this before you quote a number
Three P/L figures live on this API and they answer different questions. Quoting
the wrong one is the single most common way to be confidently wrong about a
trader.
`polymarket.pnl` is THE P/L. The chain-audited figures are the audit voice, and
they read more negative on anyone holding an open book. Two minutes here saves
a wrong verdict.
## The shape of the thing
**V1 is read-only by construction.** Every endpoint is a GET, every MCP tool is
a read. There are no writes, no orders, and no trading. A key cannot even mint
another key — that is a dashboard action.
* `{trader}` accepts a **0x wallet**, a **polymarket.com profile URL**, or a
**display name** (e.g. `GoingInsolvent`). Responses always echo the canonical
wallet and name.
* Timestamps are epoch **seconds**. Money fields are USD, suffixed `_usd`.
Prices are UP-normalized (0–1).
* Pagination is cursor-based and forward-only.
* History depth and daily trader lookups are plan-gated — see
[Plans & limits](/plans-and-limits).
## Machine-readable entry points
If you would rather hand your agent a file than a docs site:
The whole data plane in one page.
Skill format, with references and chart templates.
Every parameter and response schema.
# MCP overview
Source: https://docs.polytape.io/mcp/overview
The PolyTape MCP server — endpoint, transport, connecting, and the caching rule that bites everyone once.
```
https://api.polytape.io/mcp
```
FastMCP over **streamable HTTP**, stateless. **13 tools and one prompt.**
Works in Claude Code, Cursor, Windsurf, opencode, the claude.ai custom
connector, and any MCP client.
## Connecting
```bash theme={null}
claude mcp add --transport http polytape https://api.polytape.io/mcp \
--header "Authorization: Bearer td_live_..."
```
**Settings → Connectors → Add custom connector**, then the server URL:
```
https://api.polytape.io/mcp
```
Authorize over OAuth — you sign in with your PolyTape account, and no key is
pasted.
```json theme={null}
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"polytape": {
"type": "remote",
"url": "https://api.polytape.io/mcp",
"oauth": false,
"headers": { "Authorization": "Bearer td_live_..." }
}
}
}
```
```json theme={null}
{
"mcpServers": {
"polytape": {
"url": "https://api.polytape.io/mcp",
"headers": { "Authorization": "Bearer td_live_..." }
}
}
}
```
Both credential families work: a `td_live_` key in the `Authorization` header, or
OAuth where your client supports it. See [Authentication](/authentication).
**Re-add the connector after an API update.** MCP clients cache `tools/list`,
so new tools and sharpened tool descriptions never reach a client that
connected before the change — it will keep using the old contract and never
tell you.
If a tool you expect is missing, or a tool behaves like an older version of
itself, remove the connector and add it again. Reconnecting spends no budget.
## What a call costs
Connecting · `tools/list` · `prompts/list` · fetching the
`crack_this_trader` prompt.
A Basic user does not spend part of their 50 just to plug in.
Every tool **call**, `get_chart_template` included. There are no exemptions.
MCP and REST draw on the **same per-user budget** — see
[Plans & limits](/plans-and-limits).
**Tools return their payload twice** — as readable text *and* as structured
JSON. That is convenient, and it means paged tools cost roughly **double** their
REST equivalent. Pass `limit` / `points` wherever a tool offers one.
## Conventions
* `trader` accepts a **0x wallet**, a **polymarket.com profile URL**, or a
**display name**. Responses echo the canonical wallet and name.
* `market` is the `0x…` 64-hex condition id from `list_trader_markets` or
`get_top_markets`.
* Timestamps are epoch **seconds**. Money fields are USD. Prices are
UP-normalized (0–1).
## Errors come back readable
Failures arrive as tool results (`isError: true` with text), never as opaque
protocol errors.
| Text you'll see | What it means |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `rate limit: retry in Ns` | Transient. Wait N seconds and retry. |
| A message naming an upgrade and `polytape.io/pricing` | **The paywall** — daily (Basic) or monthly (Pro/Max) quota spent, or the Basic 3-wallets-a-day cap. Relay it to your human; do not retry. |
| `this market is outside your history window` | The depth gate — Basic sees 5 markets per trader. Surface the upgrade, or treat it as a not-found. |
| `no such tape in your library` / `no such market for this trader` | A 404-equivalent. |
| `the trader archive is not available…` / `pm_offline` | A transient upstream blip. Retry shortly; for `pm_offline`, fall back to the archive P/L fields. |
Full table with HTTP codes: [Errors](/errors).
Signatures, what each returns, and when to reach for it.
# MCP tools
Source: https://docs.polytape.io/mcp/tools
All 13 PolyTape MCP tools and the crack_this_trader prompt — signatures, what each returns, and when to reach for it.
Thirteen tools and one prompt. Every tool **call** spends 1 request;
`tools/list` and fetching the prompt spend nothing.
**The golden path:** start cheap, drill down only when a summary raises a
question.
Know your budget before you spend it.
The career line. Every trader analysis starts here.
The biggest wins, worst losses, most-traded — one scan.
The computed read on the 2–3 revealing markets.
Only if a summary looks strange — or to chart it.
***
## Orientation
### `whoami()`
Your plan (`Basic Intelligence` | `Pro Intelligence` | `Max Intelligence`), rate
limits, usage this window and this day/month, and which key is calling. Call it
first. **\~70 tokens.**
### `list_popular_traders()`
The curated popular-trader shelf: wallets, display names, and how many people are
watching each right now. Good for "who should I look at?" These stream without
holding any of your live slots. **\~50–200 tokens.**
***
## Trader analysis
### `get_trader_card(trader)`
Career stats from the on-chain archive, blended with Polymarket's own profile
stats. **Start every trader analysis here. \~120 tokens.**
| Argument | Type | Notes |
| -------- | ------ | ------------------------------------------------------ |
| `trader` | string | 0x wallet, polymarket.com profile URL, or display name |
**The headline is `polymarket.pnl`** — Polymarket's own model (realized +
unrealized, full account history including pre-V2):
`{pnl, pnl_realized, pnl_unrealized, portfolio_value, peak, low, max_drawdown,
profile_image, as_of}`. Quote it as the trader's profit.
The archive `card` block carries what Polymarket's API does not:
`markets_total`, `resolved_total`, `winrate`, `volume_usd`, `first_seen`,
`last_seen`; the career averages `avg_entry_price`, `avg_win_usd`,
`avg_loss_usd`; and the position facts `open_value_usd`, `open_markets`,
`unmarked_markets`, `marked_at`. These cover the **V2 era** until the v1
backfill.
`card.avg_entry_price` is the average cost **per token** across all buys — i.e.
the trader's **break-even winrate**, not "the price they usually pay". It is
token-weighted, so cheap-token volume drags it well below their typical
displayed entry. Compare it against `winrate` to price the edge. See
[Reading the numbers](/concepts/pnl#avg-entry-price-two-different-stats-one-name).
If Polymarket's upstream is down, the block degrades to `{"status":
"pm_offline"}` and the rest of the card still serves — **and only then** does the
card carry the archive's realized `career_pnl_usd` / `pnl_marked_usd` as the
fallback voice. An unknown-but-valid wallet returns an honest zeroed card.
### `get_top_markets(trader, n=5)`
The trader's standout markets in **one scan**: `top_wins` and `top_losses`
(realized P/L, **resolved markets only**), `most_traded` (by fills), and
`top_volume`. Each is a list of market rows. **\~1–2k tokens.**
| Argument | Type | Default | Notes |
| -------- | ------ | ------- | ---------------------- |
| `trader` | string | — | wallet, URL, or name |
| `n` | int | `5` | per slice; clamps 1–20 |
This is the whale drill-down. The recency-ordered markets list cannot sort, so
this is how you find a 40,000-market bot's biggest win. Keep `n` modest on
mega-makers — they sit near the heavy-query timeout.
### `list_trader_markets(trader, cursor=None, limit=None)`
The trader's markets, **most recent first**, one row per market with
pnl/outcome/last-action/fill count. No arrays. Paged.
| Argument | Type | Notes |
| -------- | ------ | ---------------------------------------------- |
| `trader` | string | wallet, URL, or name |
| `cursor` | string | opaque; pass back the response's `next_cursor` |
| `limit` | int | **pass a small one (e.g. 20)** |
Open (unresolved) rows also carry `market_value_usd` and `pnl_marked_usd`;
resolved rows are final.
A full page is **\~35k tokens over MCP**. `limit: 20` is \~2k. Always pass it.
Use this for *recent activity*, never for finding extremes — that is
`get_top_markets`.
Depth is plan-gated: Basic Intelligence sees the 5 most-recent markets, Pro and
Max the full history.
### `get_market_summary(trader, market)`
**The workhorse.** The computed read on one trader + market, with no arrays.
**\~250 tokens.**
| Argument | Type | Notes |
| -------- | ------ | ------------------------- |
| `trader` | string | wallet, URL, or name |
| `market` | string | `0x…` 64-hex condition id |
Returns fill/buy/sell counts and notionals, `avg_entry_price` (the **UP-space
VWAP** of buys within this market), the size distribution (`size_p50_usd` /
`size_p90_usd` / `size_max_usd`), `hold_seconds`, the share of money placed in
the first and last tenth of the market's traded window (early opener vs late
sniper), `pnl_usd` / `roi` / `won` when resolved, and a per-token `sides` block:
```json theme={null}
"sides": [
{ "token_side": 1, "outcome": "Up", "buy_count": 89, "sell_count": 0,
"buy_usd": 1820.0, "sell_usd": 0.0, "avg_entry": 0.48, "net_tokens": 3790.0 },
{ "token_side": 2, "outcome": "Down", "avg_entry": 0.55 }
]
```
`avg_entry` inside `sides` is in the token's *own* frame — what they paid —
unlike the UP-space top-level number. The `sides` block is how you see a
two-sided scalper at a glance.
Reach for this before raw events.
### `get_market_events(trader, market, ...)`
**One call returns the whole chart, sampled.**
| Argument | Type | Default | Notes |
| ----------------------------- | ------ | ------- | -------------------------------------------------------- |
| `trader` | string | — | wallet, URL, or name |
| `market` | string | — | condition id |
| `points` | int | `300` | target point count; clamps 20–600. **Ask 120 for chat.** |
| `from_ts` / `to_ts` | int | — | epoch seconds; zoom a window |
| `resolution` | int | — | force a bucket size in seconds (snapped) |
| `raw` | bool | `false` | verbatim keyset tick paging instead |
| `window` / `cursor` / `limit` | — | — | only meaningful with `raw=true` |
Response: `market` (with `outcome`, `outcome2`, `pnl_usd`, `open`, `winner`),
`trader`, a decimated `series` (`{from_ts, to_ts, fidelity, points: [[ts, last,
lo, hi], …]}`, UP-normalized), and the trader's **complete** `fills`
(`{ts, side, price, token_side, outcome, size_tokens, usd}`).
**Draw the line at `(lo + hi) / 2`** — every one of those values is a real traded
level. Overlay fills as BUY-up / SELL-down markers colored by `token_side`, sized
by `usd`. If resolved, `winner` pins the end (token-1 win → 1.00, else 0.00).
`raw=1` returns the old verbatim tick pages (`ticks` + cursor). Raw ticks
duplicate about **36×**, so this is the export path, not the chart path.
For a full-tape interactive chart, fetch the whole change-compressed tape from
the REST endpoint [`GET /traders/{trader}/markets/{condition_id}/tape`](/rest/traders#get-traders-trader-markets-condition-id-tape)
— there is no MCP tool for it.
**\~2.5–3k tokens REST / \~5–6k MCP** at 300 points.
### `get_pnl_series(trader, window="all")`
Polymarket's cumulative-P/L series for the career **equity curve**. **\~1–2k
tokens.**
| Argument | Type | Default | Notes |
| -------- | ------ | ------- | ------------------------------------------------ |
| `trader` | string | — | wallet, URL, or name |
| `window` | string | `"all"` | `24h` \| `7d` \| `30d` \| `ytd` \| `1y` \| `all` |
Returns `{trader, window, model, series: [[ts, pnl_usd], …], peak, low,
max_drawdown, as_of}`. Drawdown is a one-line derivation the equity template does
for you: `dd[i] = p[i] − running_max`. Degrades to a `pm_offline` message if the
upstream is down.
### `get_recent_fills(trader, since=None, cursor=None, limit=None)`
**The monitoring primitive.** The trader's fills across *all* their visible
markets since a timestamp, chronological, paged — and it holds **no live slot**.
| Argument | Type | Notes |
| -------- | ------ | ------------------------------------------- |
| `trader` | string | wallet, URL, or name |
| `since` | int | epoch seconds |
| `cursor` | string | pass back `next_cursor` |
| `limit` | int | **cap it** — a full page is \~10–15k tokens |
Each fill carries `condition_id`, `title`, `side`, `price`, `size_tokens`, `usd`,
`token_side`, and `outcome` — enough to fingerprint a trader in one call.
Freshness tracks the archive pipeline (minutes).
***
## Charts
### `get_chart_template(kind="market", format="html", theme="night")`
A ready-to-fill PolyTape chart template, returned as file text. This exists
because a connector cannot always fetch the skill's web files.
| Argument | Values | Notes |
| -------- | ------------------------------------- | --------------------------------------------- |
| `kind` | `market` \| `market-tape` \| `equity` | see below |
| `format` | `html` \| `python` | python = matplotlib (`market`, `equity` only) |
| `theme` | `night` \| `day` | day flips the room; the tube stays dark |
* **`market`** — the self-contained SVG template. No CDN, no external fonts, no
fetches. **The default a connector agent should use** — it renders inside a
chat artifact or a strict CSP.
* **`market-tape`** — the TradingView full-tape template (html only). Needs the
unpkg CDN and \~200KB, so file or browser contexts only.
* **`equity`** — the career equity-curve template.
Returns `{kind, format, theme, content}`. **Costs 1 request** — it is a normal
tool call, no exemptions. Fetching the same template from the web instead is
free. Full spec: [Plotting](/guides/plotting).
***
## Your tapes
Recorded watch sessions from the PolyTape web app. These are **yours** — scoped
to your account.
### `list_tapes(wallet=None)`
Your cassette library: recorded market sessions with observed PnL
(`PNL · AS TAPED`), outcome, duration, and tick/fill counts. `wallet` narrows to
one trader's folder.
The MCP tool takes **only** `wallet`. The REST endpoint
[`GET /tapes`](/rest/tapes#get-tapes) additionally accepts `limit` — reach for
REST if you need to cap a large library.
### `get_tape_summary(tape_id)`
One tape's metadata: market, grade, PNL AS TAPED, duration, tick and fill counts.
Cheap — prefer it over raw events.
### `get_tape_events(tape_id, cursor=None, types=None, limit=None)`
Raw `StreamEvent`s from a tape, paged.
| Argument | Type | Notes |
| --------- | --------- | -------------------------------------- |
| `tape_id` | string | — |
| `cursor` | string | pass back `next_cursor` |
| `types` | string\[] | e.g. `["fill"]` |
| `limit` | int | events per page — **pass a small one** |
Expensive relative to the summary; page deliberately. Oversized tapes are pointed
at a download URL instead.
***
## Prompt
### `crack_this_trader(trader)`
Not a tool — a **prompt template**, and fetching it spends no budget.
It scripts the full analyst workflow — `whoami` → card → top-markets → the
revealing few → summaries → verdict, closing with a chart if your interface
renders one — and asks for a terse, numbers-only verdict in the PolyTape voice.
Invoke it from your client's prompt picker, or paste it. In Claude Code:
```text theme={null}
Use the crack_this_trader prompt on GoingInsolvent.
```
For a looser, conversational read, use the natural
[starter prompt](/quickstart#the-starter-prompt) instead. It steers the agent
toward the card and the biggest markets, then lets it roam the tools and ends
the turn open rather than on a fixed verdict format.
# Plans & limits
Source: https://docs.polytape.io/plans-and-limits
The PolyTape ladder — agent calls, API rate, history depth, daily trader lookups, live slots, storage, and keys — plus what each wall looks like on the wire.
One budget per user. Every key you own and every OAuth session you open draws
from it.
## The ladder
**Free with every account.** Every tool, a daily taste of calls, one live
stream with full replay.
| | |
| -------------------- | ---------------------------------- |
| **Agent calls** | **50 / day** (resets midnight UTC) |
| **API rate** | 0.5 RPS · burst 10 · 30 RPM |
| **Historical depth** | 5 markets per trader |
| **Trader lookups** | 3 distinct wallets / day |
| Live streams | 1 slot |
| Watch & replay (DVR) | Included |
| Record a live market | — (Pro+) |
| Library storage | 200 MB |
| API keys | 1 |
\*\*$29 / month**, opening with a three-day trial for $7.99. The daily driver:
unlimited history, auto-saved tapes.
| | |
| -------------------- | --------------------------------------- |
| **Agent calls** | **50,000 / month** (resets month start) |
| **API rate** | 5 RPS · burst 60 · 300 RPM |
| **Historical depth** | Unlimited |
| **Trader lookups** | Unlimited |
| Live streams | 3 slots |
| Watch & replay (DVR) | Included |
| Record a live market | Auto-save |
| Library storage | 2 GB |
| API keys | 3 |
\*\*$79 / month**, opening with a three-day trial for $7.99. The desk that
never sleeps.
| | |
| -------------------- | ---------------------------------------- |
| **Agent calls** | **300,000 / month** (resets month start) |
| **API rate** | 20 RPS · burst 200 · 1,200 RPM |
| **Historical depth** | Unlimited |
| **Trader lookups** | Unlimited |
| Live streams | 5 slots |
| Watch & replay (DVR) | Included |
| Record a live market | Auto-save |
| Library storage | 10 GB |
| API keys | 6 |
Side by side:
| | Basic | Pro | Max |
| -------------------- | ------------ | --------------- | ---------------- |
| Price | \$0 | \$29 / mo | \$79 / mo |
| **Agent calls** | **50 / day** | **50,000 / mo** | **300,000 / mo** |
| **API rate** | 0.5 RPS | 5 RPS | 20 RPS |
| Burst | 10 | 60 | 200 |
| RPM ceiling | 30 | 300 | 1,200 |
| **Historical depth** | 5 / trader | unlimited | unlimited |
| **Trader lookups** | 3 / day | unlimited | unlimited |
| Live streams | 1 | 3 | 5 |
| Watch & replay (DVR) | ✓ | ✓ | ✓ |
| Record a live market | — | ✓ | ✓ |
| Storage | 200 MB | 2 GB | 10 GB |
| API keys | 1 | 3 | 6 |
**Replay is not a paid feature.** Watching a live stream and scrubbing it back —
the DVR — is on **every plan, Basic included**. What Pro and Max add is
**recording a live market**: capturing a session to a tape you keep in your
library.
So a Basic account can watch and replay all it likes; it just doesn't lay down
new tapes.
**`GET /me` is the authoritative answer.** Plans change, grants exist, and a
hand-copied table is a table that goes stale. Call
[`/me`](/rest/overview#get-me) (or the `whoami` tool) once at the start of a
run and read your real numbers off the wire.
## The four gates
Every request passes up to four checks. They are gates over one budget, not
additive allowances.
You can fire `burst` requests instantly, then one every `1/rps` seconds as
the bucket refills. This fits the natural agent shape: a handful of calls,
then idle. Overflow is `429 rate_limited` with a `Retry-After` in seconds.
A rolling 60-second ceiling that **includes** burst spend. Hitting it is the
same `429 rate_limited`. Honor `Retry-After` and continue.
The hard backstop and the upgrade lever. Basic Intelligence resets **daily**
at midnight UTC; Pro and Max reset **monthly**. Exhaustion is `429
daily_limit` / `429 monthly_limit` — see [the paywall](#the-paywall) below.
Heavy reads (tape events, market ticks) also pass a cap on in-flight heavy
queries. Overflow is an instant, honest `429 busy` rather than a queue.
Retry in about a second.
## What counts against the call quota
* Every GET = **1 request**.
* Application errors on real routes **burn a call** — a `400`, a `404`, even
a `304` on a real endpoint charges for the served work. A typo'd
`condition_id` costs a Basic user 1/50th of their day, so validate inputs
before firing.
* `429` rejections (rate limit or paywall) do **not** burn a call.
* Every tool **call** = 1 request, `get_chart_template` included. There are
no exemptions.
* **Connecting, `tools/list`, `prompts/list`, and fetching a prompt cost
nothing.** A Basic user does not spend part of their 50 just to connect,
or to invoke `crack_this_trader`.
The WebSocket plane is **slot-metered, not request-metered**. Holding a live
stream does not draw down your call count. That is a separate budget — see
"Live streams" in the ladder above.
## History depth
Basic Intelligence reaches a trader's **5 most-recent markets**. Pro and Max
reach the full record through cursor paging.
A market past the window is:
```json theme={"dark"}
HTTP 403
{ "error": "depth_gated",
"message": "this market is outside your history window" }
```
Surface the upgrade or treat it as a not-found. Do not retry — depth does not
change on its own. `history_depth` in `/me` is the count (`-1` = unlimited).
Depth is a **recency** window, not a sample. On Basic, `get_top_markets` still
ranks within the visible window — so "biggest win" means "biggest win in the
last 5 markets", and you should say so.
At five deep this matters more than it used to: five recent markets is a
glimpse, not a career. Say what you're looking at rather than generalising
from it.
## Trader lookups (Basic only)
Basic Intelligence can pull **3 distinct wallets per UTC day**. Composed with the
depth window, that reads: *three traders a day, five markets deep*.
* The counter is **distinct wallets**, not requests. Repeat reads of a wallet you
already looked at today are **free** — page it, summarize it, chart it, poll
it, all on the same one of three.
* It is **one shared counter** across the dashboard, REST, and MCP. There is no
agent bypass: a wallet you opened in the web app counts against the same three.
* It resets at midnight UTC.
* Pro and Max are unlimited.
The 4th **new** wallet that day walls on every trader-scoped read — card, market
list, market detail, summary, tape, fills, top-markets, and pnl-series:
```json REST theme={"dark"}
HTTP 403
{ "error": "wallet_quota",
"message": "…names Basic Intelligence, the upgrade, https://polytape.io/pricing, and the midnight-UTC reset…",
"used": 3,
"limit": 3,
"resets_at": "2026-07-29T00:00:00Z" }
```
```text MCP theme={"dark"}
isError: true
```
* `resets_at` is an **ISO 8601 UTC** string (next midnight UTC), not epoch
seconds. This is the one timestamp on the API that is not epoch seconds.
* There is **no `retry_after`** — that field belongs to `429`s. Here `resets_at`
carries the time.
* `message` is written to be **relayed to your human**. Surface it; do not
silently retry.
A wallet you have never read costs one of the three even if the read fails for
another reason. On Basic, resolve the trader you actually want before you spend
a slot on a guess.
## The paywall
Basic Intelligence is **50 calls a day** — enough to try a trader crack once or
twice, not to run a workflow. A well-run crack is about 6–10 calls.
When the quota is spent:
```json theme={"dark"}
HTTP 429
{ "error": "daily_limit",
"message": "You've used all 50 of your Basic Intelligence PolyTape API calls today. Pro Intelligence gives you 50,000 calls a month at https://polytape.io/pricing to keep going.",
"retry_after": 24907 }
```
Pro and Max are the same shape with `monthly_limit` and a next-month reset. On
MCP the same copy comes back as a readable tool result (`isError: true`),
plan-aware — Pro is told about Max, and so on.
**Relay the message; do not silently retry.** `retry_after` on a daily cap
points hours out and on a monthly cap points weeks out. Burning retries against
it helps nobody.
## Reacting to a 429 — the etiquette
| error | class | what to do |
| ------------------------------- | ----------- | ----------------------------------------------------- |
| `rate_limited` | transient | Honor `Retry-After` (seconds) and retry. Clears fast. |
| `busy` | transient | Admission lane full. Retry in \~1s. |
| `daily_limit` / `monthly_limit` | **paywall** | Relay the message and stop. |
| `too_many_auth_failures` | credential | Fix the key, then wait out the window. |
And the two `403` verdicts — `depth_gated` and `wallet_quota` — are **plan
walls, not outages**. Surface the upgrade. Retrying changes nothing.
**Early access — a temporary grant, not a tier.** During the early-access
period, holding a founder seat resolves your account to **Pro Intelligence
limits**. This is a grant riding a server-side switch, not a plan you are on.
At paid go-live the switch flips: founders fall back to their real plan (Basic
unless subscribed) and **keep the founder badge permanently**. The badge is
durable; the limits it currently unlocks are not.
Because of that, do not hard-code a tier for a founder account. Call `/me` and
believe what it says — it is the only surface that knows which side of the
switch you are on.
## Spending well
* Lean on **cards, summaries, and `top-markets`** — tens to \~2k tokens each, and
they already carry sizing, timing, entry pricing, and per-market P/L.
* On **MCP always pass `limit`** to `list_trader_markets` and `get_recent_fills`,
and `points` to `get_market_events`. MCP returns every result twice (text +
structured), so a default markets page is \~35k tokens where `limit: 20` is \~2k.
* **Poll politely.** `get_recent_fills` with a `since` watermark on a minutes
cadence beats a tight loop — freshness is pipeline-bound anyway.
Full detail: [Token thrift](/concepts/token-thrift).
# Quickstart
Source: https://docs.polytape.io/quickstart
Connect an agent to PolyTape in under five minutes — the claude.ai connector, any MCP client, or a raw API key.
Three ways in. They read the same data and spend the same budget — pick the one
that matches where your agent lives.
A custom connector. Sign in, no key to paste.
Claude Code, Cursor, opencode, Windsurf.
A `td_live_` key and curl.
***
## Path 1 — the claude.ai connector
The shortest route for a chat-first workflow. You sign in with your PolyTape
account instead of pasting key material.
In claude.ai, go to **Settings → Connectors → Add custom connector**.
Use the server URL:
```
https://api.polytape.io/mcp
```
Claude will walk you through OAuth. Sign in with your PolyTape account and
consent. No key is ever pasted, and no key material lives in the connector.
Where OAuth is not yet live for your client, the API-key path in
[Authentication](/authentication) always works.
Paste a trader wallet or name into the chat with a natural ask — see
[the starter prompt](#the-starter-prompt) below.
**Re-add the connector after an API update.** MCP clients cache `tools/list`,
so new tools and sharpened tool descriptions do not reach a client that
connected before the change. If a tool you expect is missing, remove the
connector and add it again. Reconnecting is free — protocol handshakes spend no
budget.
***
## Path 2 — Claude Code, or any MCP client
One line:
```bash theme={null}
claude mcp add --transport http polytape https://api.polytape.io/mcp \
--header "Authorization: Bearer td_live_..."
```
Then, in a session:
```
Use the crack_this_trader prompt on GoingInsolvent.
```
In `opencode.json`:
```json theme={null}
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"polytape": {
"type": "remote",
"url": "https://api.polytape.io/mcp",
"oauth": false,
"headers": {
"Authorization": "Bearer td_live_..."
}
}
}
}
```
Remote MCP servers default to OAuth in opencode, so `"oauth": false` is what
keeps it on the header path.
In `.cursor/mcp.json`:
```json theme={null}
{
"mcpServers": {
"polytape": {
"url": "https://api.polytape.io/mcp",
"headers": {
"Authorization": "Bearer td_live_..."
}
}
}
}
```
The bare endpoint, streamable HTTP, stateless:
```
https://api.polytape.io/mcp
```
Authenticate with `Authorization: Bearer td_live_...`, or connect over OAuth
where your client supports it.
Mint the key first — [Authentication](/authentication) has the two-click path.
***
## Path 3 — raw REST with a key
In the PolyTape dashboard: **AI Agents & API → Patch Bay → mint**. The
plaintext `td_live_` key is shown exactly once.
```bash theme={null}
curl -H "Authorization: Bearer td_live_..." \
https://api.polytape.io/api/agent/v1/me
```
You get your plan, rate limits, and usage so far. Start here so you know what
you have before you spend it.
```bash theme={null}
curl -H "Authorization: Bearer td_live_..." \
https://api.polytape.io/api/agent/v1/traders/GoingInsolvent
```
That is the career card — the start of every trader analysis.
The key goes in the `Authorization` header and nowhere else. A key is **never**
accepted in a query string: a key in a URL is a key in an access log.
***
## The starter prompt
Once connected, this is what a person would actually type. It gives the agent a
soft steer and then lets it roam the tools:
```text theme={null}
Pull up the Polymarket trader 'GoingInsolvent' with your PolyTape tools and
tell me how they make their money: what they trade, when they get in, how they
size, and whether the record reads like skill or luck. Start with their trader
card and their biggest markets, then dig into whatever looks interesting. One
thing to know: PnL here is realized at cost, so a big open position can hide in
the numbers. Give me your read, then tell me what you'd dig into next. I'll
have questions.
```
Swap the name for any wallet, profile URL, or display name.
For the scripted analyst version — the full workflow with a terse numbers-only
verdict — invoke the [`crack_this_trader` prompt](/mcp/tools#crack-this-trader)
from your client's prompt picker instead. Fetching a prompt spends no budget.
***
## Where to go next
What your plan gets you, and what walls look like when you hit one.
Which P/L to quote — read this before you write a verdict.
Payloads vary a thousandfold. Two rules save most of it.
Five worked flows with measured call and token costs.
# REST overview
Source: https://docs.polytape.io/rest/overview
Base URL, conventions, pagination, and the two orientation endpoints of the PolyTape agent REST API.
```
https://api.polytape.io/api/agent/v1
```
**Every read is a GET.** All responses are JSON. Authenticate every request with
a header:
```http theme={null}
Authorization: Bearer td_live_...
```
Full machine schema: [`/api/agent/v1/openapi.json`](https://api.polytape.io/api/agent/v1/openapi.json).
## Conventions
`{trader}` accepts a **0x wallet**, a **polymarket.com profile URL**, or a
**display name** (e.g. `GoingInsolvent`). Responses always echo the canonical
wallet and name.
A `0x`-prefixed string of the *wrong* hex length is treated as a malformed
wallet → `400 invalid_wallet`. It never falls through to name resolution.
`{condition_id}` is the `0x…` 64-hex market id from the markets list or
top-markets.
Timestamps are epoch **seconds**. Money fields are USD, suffixed `_usd`.
Prices are **UP-normalized** (0–1) — token 1's probability.
The one exception: `resets_at` on a
[`wallet_quota`](/plans-and-limits#trader-lookups-basic-only) verdict is an
ISO 8601 UTC string.
Cursors are **opaque and forward-only**. First request: no `cursor`
(optionally `since` / `window` to position). Then pass the response's
`next_cursor` back. When `next_cursor` is absent or null, you have reached the
end.
Never hand-craft or mutate a cursor. A malformed one is a clean `400
bad_cursor`; a well-formed **forged** one silently returns page 1 again — safe
depth-wise, but you re-pay for data you already have.
`limit` caps a single page and must be **≥ 1** (`limit=0` is `400 bad_limit`).
It is your main token-control knob.
Resolved market summaries and tapes are immutable and **ETag'd** — send
`If-None-Match` for a `304`.
Note that a `304` on a real route still burns a call; it saves you tokens and
bandwidth, not quota.
The trader card is `Cache-Control: no-store`.
## Plan gates
* **History depth** — Basic Intelligence reaches a trader's **5 most-recent
markets**; Pro and Max reach the full record. A market past the window is
`403 depth_gated`. `history_depth` in `/me` is the count (`-1` = unlimited).
* **Trader lookups** — Basic Intelligence can pull **3 distinct wallets per UTC
day**, shared with the dashboard and MCP. The 4th new wallet is
`403 wallet_quota`.
Both are detailed in [Plans & limits](/plans-and-limits).
***
## Orient
### `GET /me`
Plan, rate limits, usage this window and this day/month, and which key is
calling. Mirrors the MCP `whoami` tool. **Start here.**
`plan` is the display name: `Basic Intelligence` | `Pro Intelligence` |
`Max Intelligence`.
```bash Request theme={null}
curl -H "Authorization: Bearer td_live_..." \
https://api.polytape.io/api/agent/v1/me
```
```json Response theme={null}
{
"plan": "Pro Intelligence",
"limits": {
"rps": 5.0,
"burst": 60,
"rpm": 300.0,
"daily_requests": 0,
"monthly_requests": 50000,
"max_keys": 3,
"live_slots": 3,
"history_depth": -1
},
"usage": {
"window": { "tokens_remaining": 59.0, "rpm_used": 1 },
"day": { "requests": 0, "cap": 0 },
"month": { "requests": 42, "cap": 50000 }
},
"key": { "label": "my agent", "last4": "ZLI4" }
}
```
The call quota has two windows and **the governing one is nonzero**. A Basic
Intelligence caller inverts them: `daily_requests: 50`, `monthly_requests: 0`,
and `usage.day` carries the governing count and cap.
### `GET /popular`
Curated popular traders with display names and live watching counts. These stream
without holding any of your live slots.
```json theme={null}
{ "traders": [ { "wallet": "0x…", "name": "GoingInsolvent", "watching": 0 } ],
"count": 2 }
```
***
Cards, markets, summaries, charts, fills, and the equity curve.
Your recorded watch sessions.
# Tapes
Source: https://docs.polytape.io/rest/tapes
Your recorded watch sessions over REST — the cassette library, one tape's metadata, its parsed events, and the bulk export.
Base: `https://api.polytape.io/api/agent/v1`
Tapes are **yours** — recorded watch sessions from the PolyTape web app, scoped
to your account and private to it. This is the read layer for them.
Tapes are a different substrate from the archive. The archive is every
Polymarket trader's on-chain history; a tape is a session **you** recorded,
replayed as the same `StreamEvent` vocabulary the live plane emits.
**Recording a live market is a Pro/Max capability** — watching and replaying
are not, and never have been. Every plan can watch a live stream and scrub it
back; Pro and Max add laying the session down as a tape you keep.
The practical consequence for an agent: a **Basic account can legitimately have
an empty library**, and these endpoints will honestly return `count: 0`. That
is not an error and not a permissions failure — there is simply nothing on the
shelf yet. Don't retry it, and don't report it as a fault.
And don't go looking for a way to start a recording from here on any plan.
Recording is a web-app action; this API is read-only by construction, so there
is no endpoint to find.
***
## `GET /tapes`
Your cassette library with metadata: recorded market sessions with observed PnL
(`PNL · AS TAPED`), outcome, duration, and tick/fill counts.
| Query | Type | Notes |
| -------- | ------ | ------------------------------- |
| `wallet` | string | narrows to one trader's folder |
| `limit` | int | caps the newest N (must be ≥ 1) |
```json theme={null}
{ "tapes": [ … ], "count": 12 }
```
A large library is a token bomb — pass `limit`. The MCP `list_tapes` tool takes
only `wallet`, so reach for REST when you need to cap.
***
## `GET /tapes/{tape_id}`
One cassette: metadata plus summary counts. Cheap — prefer it over raw events.
```json theme={null}
{ "tape": { … } }
```
***
## `GET /tapes/{tape_id}/events`
Parsed `StreamEvent` pages.
| Query | Type | Notes |
| -------- | ------ | -------------------------------------- |
| `cursor` | string | pass back `next_cursor` |
| `types` | string | comma list, e.g. `types=fill` |
| `limit` | int | events per page — **pass a small one** |
A **heavy read**: it passes the admission lane, so overflow is `429 busy` rather
than a queue. Oversized tapes are pointed at `/download` instead of paged.
To chart a market from a tape: book-tick and last-trade events form the price
line (0–1), and the trader's fills overlay as BUY-up / SELL-down markers at
`(ts, price)`, sized by `usd`.
***
## `GET /tapes/{tape_id}/download`
A presigned `.jsonl.gz` URL, valid **1 hour** — the bulk export path.
```json theme={null}
{ "url": "https://…", "expires_in": 3600 }
```
Once signed, fetching the object costs nothing against your API budget. This is
the right way to pull a whole tape.
***
## Errors
| HTTP | error | meaning |
| ---- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| 404 | `tape_not_found` | no such tape in your library — foreign and unknown ids are the same 404, with no existence leak |
| 400 | `bad_limit` / `bad_cursor` / `unknown_event_type` | malformed request |
| 429 | `busy` | admission lane full on a heavy read — retry in \~1s |
Full table: [Errors](/errors).
# Traders
Source: https://docs.polytape.io/rest/traders
The on-chain archive over REST — career card, markets, top markets, per-market summary, the sampled chart, the full tape, the equity curve, and cross-market fills.
Base: `https://api.polytape.io/api/agent/v1`
`{trader}` accepts a 0x wallet, a polymarket.com profile URL, or a display name.
**Depth is plan-gated.** Basic Intelligence reaches a trader's 5 most-recent
markets; Pro and Max reach the full record. Basic is additionally capped at
**3 distinct wallets per UTC day**. See [Plans & limits](/plans-and-limits).
***
## `GET /traders/{trader}`
The career card, blending the on-chain archive with Polymarket's own profile.
`Cache-Control: no-store`.
```bash Request theme={null}
curl -H "Authorization: Bearer td_live_..." \
https://api.polytape.io/api/agent/v1/traders/GoingInsolvent
```
```json Response theme={null}
{
"trader": { "wallet": "0x…", "name": "GoingInsolvent" },
"card": {
"markets_total": 8305,
"resolved_total": 8305,
"winrate": 0.5624,
"volume_usd": 5780817.7,
"first_seen": 1778776327,
"last_seen": 1783189768,
"avg_entry_price": 0.49,
"avg_win_usd": 5.6,
"avg_loss_usd": -10.0,
"open_value_usd": 0.0,
"open_markets": 0,
"unmarked_markets": 0,
"marked_at": null
},
"polymarket": {
"pnl": 61240.0,
"pnl_realized": 55010.0,
"pnl_unrealized": 6230.0,
"portfolio_value": 12400.0,
"peak": 71000.0,
"low": -3400.0,
"max_drawdown": 42000.0,
"profile_image": "https://…",
"as_of": 1783200000
}
}
```
**`polymarket.pnl` is THE P/L** — Polymarket's model, realized + unrealized, full
account history including pre-V2. If the upstream is down, `polymarket` becomes
`{"status": "pm_offline"}`, the card still serves, and **only then** does it carry
the archive's realized `career_pnl_usd` / `pnl_marked_usd` as the fallback voice.
`card.avg_entry_price` is the average cost **per token** across all buys — the
trader's **break-even winrate**, token-weighted. It is not the price they usually
click. `avg_win_usd` / `avg_loss_usd` are per-resolved-market averages, and one
blowup skews them.
`avg_entry_price`, `avg_win_usd`, and `avg_loss_usd` are **absent or null until
the career-stats rollup has populated a trader**. Treat missing as
"unavailable", never as zero.
`winrate` is null when nothing has resolved. An unknown-but-valid wallet returns
an honest zeroed card, not a 404.
Full doctrine: [Reading the numbers](/concepts/pnl).
***
## `GET /traders/{trader}/top-markets`
The trader's standout markets in **one scan** — the whale drill-down the
recency-only markets list cannot do.
| Query | Type | Default | Notes |
| ----- | ---- | ------- | ---------------------- |
| `n` | int | `5` | per slice; clamps 1–20 |
```json theme={null}
{ "trader": {…},
"top_wins": [ , … ],
"top_losses": [ … ],
"most_traded": [ … ],
"top_volume": [ … ],
"note": "wins/losses rank realized pnl over RESOLVED markets only" }
```
Each row uses the markets-list shape below. Uncached, since open markets shift.
Keep `n` modest on 100k-market makers — they sit near the heavy-query timeout
(`504 query_timeout`).
***
## `GET /traders/{trader}/markets`
Recency-ordered markets, one row each.
| Query | Type | Notes |
| -------- | ------ | ------------------------------------------------------ |
| `cursor` | string | opaque; pass back `next_cursor` |
| `limit` | int | caps the page (default \~200) — **pass a smaller one** |
Each row: `condition_id`, `title`, `outcome`, `resolved`, `winner`,
`last_traded_at`, `trader_last_action` (`BUY`/`SELL`), `last_price`,
`fills_count`, `pnl_usd`, `open`, `volume_usd`. Open rows also carry
`market_value_usd` and `pnl_marked_usd`.
For extremes use `/top-markets`, never paging. This list is **recency-ordered
only** by design — a 40,000-market bot has no "biggest win" on page one.
***
## `GET /traders/{trader}/markets/{condition_id}/summary`
The computed stat block. **Prefer this over raw events.** No arrays.
```json theme={null}
{
"market": { "condition_id": "0x…", "title": "…", "outcome": "Up", "outcome2": "Down",
"resolved": true, "winner": "Down", "pnl_usd": -21.96, "open": false },
"summary": {
"fills_count": 148, "buy_count": 148, "sell_count": 0,
"buy_usd": 360.85, "sell_usd": 0.0, "avg_entry_price": 0.3302,
"size_p50_usd": 2.5, "size_p90_usd": 4.15, "size_max_usd": 4.85,
"first_fill_ts": 1783188908, "last_fill_ts": 1783189141, "hold_seconds": 233,
"usd_share_first_tenth": 0.0, "usd_share_last_tenth": 0.4145,
"pnl_usd": -21.96, "roi": -0.0609, "open": false, "won": false,
"sides": [
{ "token_side": 1, "outcome": "Up", "buy_count": 89, "sell_count": 0,
"buy_usd": 1820.0, "sell_usd": 0.0, "avg_entry": 0.48, "net_tokens": 3790.0 },
{ "token_side": 2, "outcome": "Down", "avg_entry": 0.55 }
]
},
"trader": { "wallet": "0x…", "name": "GoingInsolvent" }
}
```
| Field | Meaning |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `avg_entry_price` | **UP-space** VWAP of the trader's buys in this market |
| `sides[].avg_entry` | the same idea in the token's **own** frame — what they paid |
| `usd_share_first_tenth` / `_last_tenth` | fraction of their USD placed in the first / last 10% of the market's whole traded window. Near-zero first + high last = a late piler. |
| `roi` | pnl ÷ buy notional — **meaningless for split-sellers** |
Resolved markets are immutable and ETag'd — send `If-None-Match` for a `304`.
***
## `GET /traders/{trader}/markets/{condition_id}`
**The sampled market chart in one call** — no cursor, no tick flood.
| Query | Type | Default | Notes |
| ------------------- | ---- | ------- | ----------------------------------------------------------- |
| `points` | int | `300` | target count; clamps 20–600, snapped to the fidelity ladder |
| `from_ts` / `to_ts` | int | — | epoch seconds; zoom a window |
| `resolution` | int | — | force a bucket size in seconds (snapped) |
| `raw` | bool | `false` | verbatim keyset tick paging instead |
```json theme={null}
{
"market": { "condition_id": "0x…", "title": "…", "outcome": "Up", "outcome2": "Down",
"resolved": true, "winner": "Up", "pnl_usd": 81.08, "open": false },
"trader": { "wallet": "0x…", "name": "GoingInsolvent" },
"series": { "from_ts": 1783189500, "to_ts": 1783189800, "fidelity": 1,
"points": [ [1783189500, 0.5088, 0.47, 0.51], … ] },
"fills": [ { "ts": 1783189505, "side": "BUY", "price": 0.48, "token_side": 2,
"outcome": "Down", "size_tokens": 5.0, "usd": 2.6 }, … ]
}
```
* `points` are `[ts, last, lo, hi]`, UP-normalized. **Draw `(lo + hi) / 2`** —
every value is a real traded level; the server never emits a computed price.
* Omit `from_ts` / `to_ts` for the title-anchored default window (a "2:25–2:30PM
ET" market opens at the title's start).
* `fills` is the trader's **complete** fill set, always present, each carrying
`token_side` and `outcome`.
`raw=1` returns the old verbatim contract instead: `ticks: [{ts, price}]` plus
`window` / `cursor` / `limit` keyset paging. Raw ticks duplicate about **36×**,
so this is the export path. For a chart you want the sampled default.
***
## `GET /traders/{trader}/markets/{condition_id}/tape`
The whole market as a **change-compressed per-second tape** in one gzipped
response (`Content-Encoding: gzip`), uncapped — for the interactive TradingView
template.
```json theme={null}
{ "market": {…}, "trader": {…}, "tape": [ [ts, last, lo, hi], … ] }
```
Rows are UP-normalized and emitted only when a second's `(last, lo, hi)` differs
from the previously emitted second. A 53-day market is roughly 27,000 rows /
\~150KB gzipped, not millions. Do all bucketing and the band-mid walk client-side.
Fine for a file or a browser; **too big for a chat artifact**. For chat, use the
sampled endpoint above with `points=120`.
There is no MCP tool for this endpoint — it is REST-only.
***
## `GET /traders/{trader}/pnl-series`
Polymarket's cumulative-P/L series for the career **equity curve**.
| Query | Values | Default |
| -------- | ------------------------------------------------ | ------- |
| `window` | `24h` \| `7d` \| `30d` \| `ytd` \| `1y` \| `all` | `all` |
```json theme={null}
{ "trader": {…}, "window": "all", "model": "polymarket_profile",
"series": [ [1733000000, 0.0], [1733086400, 152.4], … ],
"peak": 231000.5, "low": -12400.2, "max_drawdown": 88211.0,
"as_of": 1752710000 }
```
Drawdown is a one-line derivation the equity template does for you:
`dd[i] = p[i] − running_max`. Degrades to `{"status": "pm_offline"}` if the
upstream is down.
***
## `GET /traders/{trader}/fills`
Cross-market fills since a timestamp — **the monitoring primitive**. Any wallet,
no live-slot cost, freshness bounded by the archive pipeline (minutes).
| Query | Type | Notes |
| -------- | ------ | ------------------------------------------- |
| `since` | int | epoch seconds |
| `cursor` | string | pass back `next_cursor` |
| `limit` | int | **cap it** — a full page is \~10–15k tokens |
Each fill: `{ts, condition_id, title, side, price, token_side, outcome,
size_tokens, usd}`.
Record the last fill's `ts` and pass it back as `since` next poll, so you only
pull what is new. Poll on a **minutes** cadence — see
[Monitor a wallet](/guides/recipes#monitor-a-wallet).