Skip to main content
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

Two market-chart flavors, routed by where the chart will live:

Chat / MCP connector

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").

File / browser

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/, 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:
1

The price line — Polymarket blue #3d7dff

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.
2

The traded band

A faint blue fill between lo and hi. It shows where trading actually happened; the line rides its middle.
3

The fills — ▲ buy / ▼ sell, colored by TOKEN, sized by $

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.
4

The resolution pin

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.
5

An honest y-scale

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. 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:
  • 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_sideoutcome 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 applies to every chart you emit:
  • A resolved market’s pnl_usd is a result — “won X"/"lostX" / "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"wonX") — 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.3kmarketcanshowbuy42.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:
window24h | 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:
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. 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

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.