Skip to content

Generate your Agent API Key

Free users can generate a read-only Agent API Key for MCP tools with signals:read and 200 MCP requests/day. Pro adds real-trade REST/MCP scopes; Elite adds full agent write access.

PolyTrackers API (External + AI Agent Guide)

Agent authentication discovery: /auth.md describes human-issued Agent API Keys. A rejected POST /api/mcp returns its existing JSON-RPC 401 error with WWW-Authenticate: Bearer realm="PolyTrackers API", resource_metadata="https://polytrackers.com/.well-known/oauth-protected-resource/api/mcp". The metadata binds to the challenged resource. This is API-key discovery, not OAuth delegation or anonymous agent registration.

This is the production-facing API guide for external developers and AI agents.


Scope

This guide/spec covers the full AI agent surface:

  • Auth: /api/auth/refresh
  • Account/Tier: /api/account, /api/account/stats, /api/account/streak, /api/account/risk-profile
  • Discovery: /api/markets, /api/markets/end-dates, /api/market-signals, /api/leaderboard, /api/leaderboard/internal, /api/leaderboard/internal/me, /api/clob/price, /api/clob/prices, /api/clob/price-history, /api/clob/fee-info, /api/trader-lookup
  • Anomalies: /api/anomalies, /api/anomalies/{id}, /api/anomalies/by-market, /api/anomalies/performance, /api/receipts/recent, /api/scan/trigger, /api/scan/run
  • Backtesting: /api/backtest
  • Recommendations, activity & feedback: /api/recommendations, /api/bets/sync, /api/feedback
  • Agent API Keys: /api/keys, /api/keys/generate, plus public feeds
  • Trading: /api/trades, /api/trade/execute, /api/trade/history, /api/trade/ledger, /api/trade/money-summary, /api/trade/orders*
  • Alerts & webhooks: /api/alerts/preferences, /api/webhooks*
  • Whale service + ingest webhooks: wallets/roster/whale control + /api/whale/webhook*
  • Health: /api/health (anonymous, unmetered liveness + database probe)

Excluded intentionally:

  • /api/mock/* internal/MCP-only simulation helpers (use the registered pt_mock_* tools)
  • /api/cron/* scheduled internal jobs
  • /api/admin/*, /api/stripe/*, /api/telegram/*, /api/onboarding/* and other UI/operator-only routes

The MCP-only pt_whale_forward_returns_get read accepts 1–50 arbitrary wallet entries with exact per-wallet since timestamps and a shared as_of cutoff. It returns one request-ordered row per entry with bounded fill coverage, gross/post-cost capital-accounting ROI, unresolved capital, resolution provenance, and isolated ok|partial|error state. Its Data API and Gamma calls use the low-priority backfill request budget and are counted in _budget. Zero resolved capital produces null ROI; compute cohort medians client-side over non-null rows. Re-runs at the same cutoff may incorporate later authoritative Gamma outcome/closedTime evidence as corrected upstream data.

The Elite, read-only pt_copy_replication_aggregates_get tool is MCP-only, operator-allowlisted through ADMIN_EMAILS, and has no public REST or browser route. Ordinary customer keys fail closed. It requires one exact whale, fixes the service-role Copy Receipt V1 RPC limit at one, and uses index-backed lookup with a caller-side five-second deadline that aborts the PostgREST request. Scored metrics stay null below 20 distinct settled conditions and high confidence still requires 40+ conditions with at least 75% complete receipts. Do not present it as a public copy score or recommendation.

GET /api/recommendations includes the stored botLikeness aggregate. A high tier adds a copy-fill-lag warning and demotes matchScore; the response never includes raw Polymarket trade history. Rows also include tradingStyle, a deduped BUY entry-price distribution with sample/window metadata. Its badge classification is withheld below 20 fills; sufficiently sampled distributions also contribute to deterministic risk-fit scoring. Recommendation rows also expose name_source (tracked_alias, leaderboard, or address_fallback), name_as_of, and Polymarket's rename-stable pseudonym when available. Address-prefixed legacy aliases are treated as absent; wallet address remains the identity key.

GET /api/leaderboard and GET /api/recommendations include upstream metadata for the shared Polymarket leaderboard fetch. Check its per-timeframe outcomes plus degraded and stale before interpreting an empty candidate pool. A stale response is explicitly labeled, retains the original fetchedAt timestamp, and is bounded to the latest fully healthy snapshot from the prior 24 hours. staleExpiresAt is the absolute cutoff; public, REST, and MCP response layers do not cache leaderboard payloads beyond the durable 10-minute Redis cadence, and each response revalidates after downstream enrichment so an embedded fallback is removed at that cutoff. Candidate actionability also distinguishes known dormancy from missing ingestion coverage: zero local activity inside the warning window for an untracked, newly tracked, or recently resumed wallet returns warning-only ACTIVITY_COVERAGE_UNKNOWN plus recent_activity.source/coverage metadata, never DORMANT_WHALE. Discovery rows expose bankroll ROI, arithmetic average per-trade return, entered/used counts, source truncation, and first/last loaded signal as separately named capital-accounting fields. Truncated or fewer-than-20-entry canonical runs are reporting-only insufficient_coverage; a shorter complete diagnostic does not clear the 90- or 180-day admission gate. MCP capability revisions are whale-coverage-honesty-v11 for the leaderboard, whale-coverage-honesty-v9 for recommendations, copy-digest-entry-fill-v18 for the copy-trading digest, and whale-coverage-honesty-v4 for the backtest tool. Wallet-shaped suggestion/trader rows use the canonical recent-flow majority predicate, filter majority-ephemeral and unknown-coverage rows under durable_only, retain dormant wallets, and expose bounded durability_profile provenance.

Recommendation and digest-suggestion rows expose previously_removed_by_caller by normalized wallet address. It is null or contains { count, last_removed_at, last_reason }, where count covers currently inactive assignments across the caller's mock wallets rather than lifetime removal cycles. These candidates remain visible with a warning by default. REST accepts exclude_previously_removed=true|false; direct MCP accepts the boolean exclude_previously_removed and binds it to continuation cursors. Filtered counts appear as omitted_by_prior_removal_filter.

PATCH /api/roster/{address} treats fromWalletId as the source-assignment selector. If one address has active assignments on multiple visible wallets, an address-only write fails with HTTP 409, code AMBIGUOUS_ASSIGNMENT, and candidate_wallet_ids rather than changing an arbitrary row. Cookie-authenticated PATCH requests and POST /api/whale/roster-assign requests return HTTP 403 when browser origin headers indicate a cross-site write. The WHALE_SERVICE_SHARED_SECRET path for PATCH is unaffected.

POST /api/roster accepts any exact Polymarket whale wallet address; the whale does not need to appear in leaderboard or recommendation results. The walletId may select a caller-owned paper roster or dedicated real-money copy roster returned by GET /api/wallets. name is optional, and copyEnabled:false stages the assignment watch-only. When starting from a username, resolve and review the exact address with GET /api/trader-lookup before creating the assignment. Username resolution uses Polymarket's public profile search, then fetches and validates trades for only that canonical wallet; trade pages are never used to infer username ownership.

Roster rows also expose maxMirrorsPerDay (and max_mirrors_per_day). POST /api/roster, POST /api/whale/roster-assign, and PATCH /api/roster/{address} accept an optional integer from 0 to 100000; 0 is the default and means unlimited. The cap is scoped to one wallet and whale, counts original copy BUY entries in the current UTC day, and does not count partial SELL activity. A positive mock cap is enforced by the atomic trade insert; real copy reads use real_trades.whale_address for advisory evaluation. Set fromWalletId when patching an address assigned to multiple wallets.


Auth modes

Most user-scoped agent endpoints accept authenticated user context via:

  • Supabase session cookie (sb-* managed cookies)
  • Authorization: Bearer <supabase-jwt-access-token>

2) JWT-only bearer

POST /api/keys/generate strictly verifies Supabase JWT bearer identity.

3) Agent API Key bearer

Agent API Keys are prefixed ptk_ and are passed as Authorization: Bearer <agent-api-key>. The same key works for PolyTrackers MCP server access and direct REST/API requests when it has the required scopes:

  • signals:read — read-only data, including caller-owned private account/trade receipts on supported MCP tools (not a public-data-only scope)
  • agent:scan — scan trigger where allowed
  • trade:execute — real trade execution only, after readiness and preflight gates pass
  • agent:full — full agent/write/trade access (currently Elite-only for Agent API Keys)

Endpoints that accept Agent API Keys:

EndpointRequired scopeDirect REST tier
GET /api/anomaliessignals:readPro+
GET /api/tradessignals:readPro+
GET /api/leaderboardsignals:readPro+
POST /api/backtestsignals:readFree+ (fixed demo config on Free)
GET /api/anomalies/performancesignals:readPro+
GET /api/public/whale-signalssignals:readElite
GET /api/public/copy-signalssignals:readElite
POST /api/trade/executetrade:executePro+
GET /api/trade/historyagent:fullPro+ (Elite for new full keys)
GET /api/trade/ledgeragent:fullPro+ (Elite for new full keys)
GET /api/trade/money-summaryagent:fullPro+ (Elite for new full keys)
GET /api/trade/orders, GET /api/trade/orders/{id}agent:fullElite
DELETE /api/trade/orders/{id}agent:fullPro+
POST /api/feedbackagent:fullElite

The direct REST tier applies only when the bearer is a ptk_ Agent API Key; session-authenticated callers follow the tier behavior below. Free Agent API Keys work on POST /api/backtest, but only with the fixed demo configuration, and the returned trade list is capped at 5 rows. Every other row requires current Pro or Elite access: the key validator rejects a Free key before the handler runs, so GET /api/trades answers a Free key with a plain 401 ({ "error": "Unauthorized" }), not TIER_UPGRADE_REQUIRED. Free-tier agents read mock and real trade history through MCP pt_trades_list (signals:read, Free) instead of GET /api/trades. New agent:full keys are normally issued only to Elite accounts; the Pro+ cancellation tier allows an existing agent:full key to keep reducing exposure after an account downgrade.

GET /api/trade/history keeps its existing REST Agent API Key gate: agent:full plus the validator's current Pro/Elite access check. Browser/Supabase JWT users can read their own receipts at every tier without an Agent API Key. For least-privilege agent reads, use MCP pt_trade_history_get with signals:read at any tier; do not add write scopes to a key just to read history. Both interfaces fence the same real_trades projection to the authenticated user, regardless of extra owner/wallet query parameters. See the trade-history permission contract for the fields, compatibility rationale and authorization tests.

4) Service auth

For whale-service control endpoints:

  • x-whale-service-secret: <secret> or
  • Authorization: Bearer <WHALE_SERVICE_SHARED_SECRET>

5) Scan secret

POST /api/scan/run also accepts x-scan-secret: <SCAN_SECRET> (bypasses the tier gate; used by cron/internal automation).

6) Webhook signature auth

POST /api/whale/webhook requires:

  • x-alchemy-signature = HMAC_SHA256(raw_body, ALCHEMY_WEBHOOK_SIGNING_KEY)

When a registration has a secret, outbound deliveries from /api/webhooks* include X-Webhook-Signature with the value sha256=<HMAC_SHA256(raw_body, secret)>. Compute the HMAC over the raw request body before parsing it.


Tier gating (important)

Effective tier resolution: a user on free with an active trial (trial_ends_at > now) is treated as pro. Tier failures return TIER_UPGRADE_REQUIRED with the required tier and an upgradeUrl.

Free

  • GET /api/trades: 7-day mock history plus full-depth own real-trade receipts.
  • GET /api/anomalies: max 5 results, 7-day base retention window — each activated referral adds 7 days, up to 28 days total — with a 1-hour visibility delay (detections newer than 1 hour are hidden).
  • POST /api/scan/trigger: not allowed (403).
  • /api/keys + POST /api/keys/generate: allowed; generated keys always get exactly signals:read (requested write/automation scopes are stripped). Limit: 1 active Agent API Key per user; rotate by revoking the old key, then generating a replacement. MCP usage is capped at 10 requests/min per tool and 200 requests/day total.
  • POST /api/trade/execute: session callers can execute only when manual trading-readiness checks pass. Agent API Key/MCP execution is unavailable because Free keys never carry trade:execute.
  • Session callers can read their own /api/trade/history, /api/trade/ledger, and /api/trade/money-summary; direct Agent API Key calls still require agent:full. Order listing/detail remain unavailable. DELETE /api/trade/orders/{id} may cancel the signed-in user's own resting order because cancellation reduces exposure.
  • /api/webhooks (create): not allowed (403).
  • /api/public/whale-signals, /api/public/copy-signals: not allowed (403).

Pro

  • GET /api/trades: 90-day mock history plus full-depth own real-trade receipts.
  • GET /api/anomalies: max 100 results, 90-day retention window, no delay.
  • POST /api/scan/trigger: allowed, rate limited to 2/hour.
  • /api/keys + POST /api/keys/generate: allowed; scopes default to signals:read, with optional trade:execute for real trade execution and agent:scan for scan automation where allowed. Limit: 1 active Agent API Key per user; rotate by revoking the old key, then generating a replacement.
  • /api/webhooks (create): up to 5 active webhooks.
  • /api/public/whale-signals, /api/public/copy-signals: not allowed (Elite only).
  • POST /api/trade/execute: session callers can execute only when manual trading-readiness checks pass. Agent API Key/MCP execution requires trade:execute scope.
  • Session callers can read their own /api/trade/history, /api/trade/ledger, and /api/trade/money-summary; direct Agent API Key calls still require agent:full. Order listing/detail remain unavailable. DELETE /api/trade/orders/{id} may cancel the signed-in user's own resting order.

Elite

  • GET /api/trades returns full-depth mock and own real-trade history.
  • GET /api/anomalies: max 200 results, full retention window.
  • POST /api/scan/trigger: allowed, rate limited to 10/hour.
  • /api/keys + POST /api/keys/generate: scopes may include signals:read, trade:execute, agent:scan, and/or agent:full. Limit: 1 active Agent API Key per user; rotate by revoking the old key, then generating a replacement.
  • /api/trade/execute, /api/trade/history, /api/trade/ledger, /api/trade/money-summary, and /api/trade/orders*: allowed where the route-specific readiness checks pass (trade:execute or agent:full for execute API-key auth). Browser/session order cancellation is allowed for the signed-in user's own resting order regardless of tier.
  • /api/public/whale-signals, /api/public/copy-signals: allowed.
  • /api/webhooks (create): up to 50 active webhooks.

Rate limits

Agent API Key REST limits use the scope shown below; MCP separately applies per-identity/per-tool and aggregate request buckets (see MCP rate limits). Session requests are limited per user, while the public discovery and refresh limits are per IP.

EndpointLimitScope
POST /api/auth/refresh30/minuteIP
Anonymous discovery GETsshared 60/minute anonymous budgetIP
POST /api/scan/triggerPro: 2/hour, Elite: 10/houruser
POST /api/keys/generate5/hour per useruser
POST /api/trade/execute10/minute per user + 20/minute per API keyuser + key
GET /api/public/whale-signals120/minute per API keyAPI key
GET /api/public/copy-signals120/minute per API keyAPI key

The shared anonymous discovery budget covers GET /api/markets, GET /api/events, GET /api/home-snapshot, and GET /api/market-signals. One anonymous caller consumes the same IP-scoped 60-request bucket across all four routes.

Rate limit response headers

Every rate-limited response — a success as well as a 429 — carries the RFC RateLimit-* quota view for the bucket that governed it. Read it and pace yourself; do not discover the ceiling by being refused.

HeaderMeaning
RateLimit-LimitEffective ceiling for this caller, after tier multipliers (Pro x2, Elite x5).
RateLimit-RemainingRequests left in the current window.
RateLimit-ResetDelta-seconds until the quota refills. Never a unix timestamp.
RateLimit-Policy"bucket";q=<limit>;w=<window seconds> — endpoints sharing a bucket share one budget.
bash
curl -si https://polytrackers.com/api/markets | grep -i '^ratelimit-'
RateLimit-Limit: 60 RateLimit-Remaining: 59 RateLimit-Reset: 60 RateLimit-Policy: "public-market-read";q=60;w=60

Rules a client can rely on:

  • On a 429, Retry-After stays the authoritative back-off signal, and RateLimit-Reset is never smaller than it. Sleeping for RateLimit-Reset is therefore always safe.
  • RateLimit-Reset rounds up, so it never expires early.
  • Paths with no limiter behind them omit the headers entirely rather than advertise a quota nobody enforces: /api/health, /api/cron/*, the signed provider webhooks, and the cached GET /api/mcp capability probe.
  • A few in-route limiters signal with Retry-After and a structured body but publish no quota view: the POST /api/trade/execute per-API-key limiter, POST /api/auth/refresh (whose 429 can be proxied from upstream), the GET /api/clob/price-history per-IP limiter, and the GET /api/markets/end-dates per-user limiter (which sends legacy X-RateLimit-Limit / X-RateLimit-Remaining headers instead). Treat a missing quota view as "unknown budget", never as "unlimited".
  • The MCP per-user/per-tool buckets are metered in-route and report retry_after_seconds inside the JSON-RPC error payload; the RateLimit-* headers on /api/mcp describe the per-IP transport cap only.

MCP rate limits

MCP (POST /api/mcp) is metered separately from the REST limits above, in three per-user layers plus one transport cap. All apply at the same time; the tightest one binds.

BucketFreeProEliteScope
Per-tool tools/call10/minute60/minute300/minuteuser, per tool name
Daily tools/call quota (all tools)200/dayno daily capno daily capuser
All JSON-RPC requests (setup + listings)120/minute600/minute1800/minuteuser

Transport caps on /api/mcp apply per IP before authentication, regardless of tier: 300/minute for authenticated traffic and 60/minute for the anonymous health probe and handshakes.

  • The per-tool bucket is keyed by tool name, so a burst on one tool does not consume another tool's budget. Blocked calls return RATE_LIMITED with retry_after_seconds.
  • The daily quota is consumed only after the per-minute bucket allows the call, so minute-throttled calls do not burn the daily budget.
  • The request bucket charges every authenticated POST one setup unit, plus one extra unit for catalog listings and resource/prompt reads. tools/call, initialize, ping, and notifications/* pay only the setup unit.
  • The transport IP caps are enforced before JSON-RPC handling and may return a non-JSON 429; treat any raw 429 as RATE_LIMITED and back off.

The same figures are published machine-readably in the polytrackers://mcp/capabilities resource (rate_limit_per_minute, rate_limit_per_day, transport_rate_limits.streamable_http).

404 response shape (unknown paths)

Any /api/** path that matches no endpoint returns HTTP 404 with a JSON body. It is never the HTML app shell, so a client can branch on the payload:

json
{
  "error": "Not Found",
  "code": "NOT_FOUND",
  "message": "No PolyTrackers API endpoint matches GET /api/nope. The OpenAPI contract lists every documented path.",
  "documentation": {
    "openapi": "https://polytrackers.com/openapi.json",
    "apiDocs": "https://polytrackers.com/docs/api",
    "llmsTxt": "https://polytrackers.com/llms.txt",
    "agentSkill": "https://polytrackers.com/skill.md",
    "mcp": "https://polytrackers.com/api/mcp"
  }
}

Send Accept: text/markdown for the same recovery links as a short markdown document. Both variants set Vary: Accept. NOT_FOUND is not retryable — re-read the OpenAPI contract instead of backing off.

Page (non-/api) paths keep serving the regular HTML 404 page.

Markdown content negotiation (public pages)

A curated set of public pages answers Accept: text/markdown with a short machine-readable summary instead of HTML — the page title, its description, and the discovery links to read next. The set is every static public page — the homepage, the documentation hub and API reference, the guides, the comparison pages, and the policy pages — reachable either by sending the header or by appending .md to the path (/docs -> /docs.md). Live-data pages (the leaderboard, anomalies, wallet tracker, analytics, search, and copy trading) are excluded on purpose: a summary of a continuously refreshed page would carry none of its data. public/llms.txt publishes the live list.

bash
curl -s -H 'Accept: text/markdown' https://polytrackers.com/docs/api

Rules:

  • The markdown body is a summary, not a transcription. Fetch the canonical HTML URL it names when you need the full document.
  • Both representations set Vary: Accept, so a shared cache cannot mix them.
  • Ranking honors q-values. text/markdown must outrank text/html; a tie serves HTML, because a client that accepts both can render the page.
  • An Accept that matches neither representation and carries no wildcard gets 406 Not Acceptable. A missing or wildcard Accept gets HTML.
  • Every other path ignores the header and serves HTML exactly as before.

429 response shape

Every 429 body includes "error" (string). The Retry-After header (seconds) is the authoritative back-off signal — it is present on all rate-limited responses (the one exception: 429s proxied from the upstream auth provider on POST /api/auth/refresh may omit it). Other body fields vary by endpoint, so treat them as optional:

Endpoint / limiter429 body
GET /api/public/whale-signals{ error }
GET /api/public/copy-signals{ error }
POST /api/trade/execute (per API key){ error, code: "API_KEY_RATE_LIMITED", retryAfter, docsUrl }
POST /api/trade/execute (per user){ error, code: "RATE_LIMITED" }
POST /api/keys/generate{ error, code: "RATE_LIMITED" }
POST /api/auth/refresh{ error, code: "RATE_LIMITED" }
POST /api/scan/trigger{ error }

The fullest shape — returned by the /api/trade/execute per-API-key limiter:

json
{
  "error": "This API key has exceeded its trade execution rate limit. …",
  "code": "API_KEY_RATE_LIMITED",
  "retryAfter": 7,
  "docsUrl": "https://polytrackers.com/docs/api"
}

Idempotency for /api/trade/execute

Session-authenticated trade execution enforces the configured site origin: Sec-Fetch-Site: cross-site or a mismatching Origin returns HTTP 403 with code TRADE_EXECUTE_CROSS_ORIGIN, before body parsing, idempotency locking, or trade execution. Requests without either browser origin header retain existing behavior. Agent API Key requests are exempt from this session check.

Trade execution is idempotent when you supply an Idempotency-Key header. For Agent API Key requests the header is required (400 IDEMPOTENCY_KEY_REQUIRED otherwise); session requests may still pass idempotencyKey in the body. Every /api/trade/execute body must also include regionCertification: true to certify the caller is permitted to trade in their jurisdiction and is not in a restricted or prohibited Polymarket trading region.

  • Accepted formats: UUIDv4 (preferred) or any 8–256 printable-ASCII token.

  • Retrying the same key replays the original trade result (200 with idempotencyReused: true) instead of re-executing. The replay window is indefinite — keys never expire. Generate one fresh key for each new trade intent. Reuse that same key for every retry or reconciliation attempt for that intent, including after an uncertain timeout or a TRADE_IN_FLIGHT response; never reuse the key for a different trade intent.

  • Two concurrent requests with the same key return 409 TRADE_IN_FLIGHT for the loser:

    json
    {
      "error": "trade_in_flight",
      "code": "TRADE_IN_FLIGHT",
      "idempotencyKey": "agent-trade-2026-04-20-001",
      "retryAfter": 5
    }

Lifecycle contradictions and order-book indicators

Agent API and MCP clients must treat Gamma lifecycle fields as conservative write gates. When pt_mock_price_get or a composed market-intel/copy-desk workflow reports PAST_END_DATE_BUT_MARKET_APPEARS_LIVE, that is contradiction evidence only; it is not permission to place, mirror, or override by default. Live CLOB/order-book indicators, bid/ask/spread rows, recent trades, or enableOrderBook signals do not override MARKET_PAST_END_DATE, MARKET_CLOSED, auth, tier, anti-IDOR, preflight/idempotency, wallet-readiness, region, or real-money execution safeguards.

Resolved markets are separately readable for settlement. When Gamma has one unique terminal 0/1 outcome, pt_mock_price_get adds an optional resolution block with winning_outcome, winning_token_id, ordered outcome/token/payout rows, and resolved_at. When the live CLOB book is unavailable but its market metadata marks one winning token, pt_clob_price_get returns status:"resolved" with that winner instead of a generic unavailable result. This is settlement evidence only: priceable and all entry/write gates remain unchanged, and open or ambiguous markets expose no winner.

Marking an existing position is not a write gate, and one narrow case is handled separately. Daily markets — the weather book's "highest temperature in <city> on <date>" questions — carry an end_date at the START of the observation day, up to ~24h before they resolve, and keep trading the whole time. pt_mock_price_get therefore prices a past-end-date market when it still looks like a same-day market mid-observation on every axis Gamma reports: inside a 36h grace window, not closed, not archived, active, accepting orders, order book enabled, a row updated within the hour, and coherent two-sided outcome prices. Missing or contradicting any one of those keeps the conservative non-priceable answer, so a long-dead market whose Gamma flags still read live stays unpriceable. lifecycle_reason reports MARKET_PAST_END_DATE either way; manual entry gates retain this conservative valuation contract. Copy execution uses the separate provider lifecycle contract below.

A future manual Agent API/MCP write exception must prove the Gamma lifecycle field is stale, require explicit user/operator approval for the exact action, write an audit log, and must not broaden default Agent API/MCP write privileges.

Copy execution (paper and real) does not infer closure from Gamma's scheduled end date. For a past-date market, explicit closed: false and acceptingOrders: true, with no resolved/archived/inactive state, permits the ordinary copy risk path. Lifecycle evidence records end_date_stale: true; closed or non-accepting markets still emit MIRROR_SKIPPED_MARKET_PAST_END. Missing flags remain lifecycle-unknown. Existing priceability, CLOB order, authorization and wallet risk checks still apply.

Activity metadata includes bounded marketLifecycle evidence: reason, source, checkedAt, endDate, end_date_stale, active, closed, archived, acceptingOrders, and umaResolutionStatus. Digest lifecycle summaries preserve the checked time and stale-date marker and do not infer a past-end verdict from the schedule when that stored observation confirms an open market. Cursor/byte-compacted rows retain marketLifecycle. This is historical decision evidence, not a current quote or permission to place a new trade; legacy rows and explicit priceability/closure warnings keep their existing conservative interpretation.

Bot challenge (403)

Session traffic that BotID flags as automated receives:

json
{
  "error": "bot_challenge_failed",
  "reason": "automated_traffic_detected",
  "suggestedAction": "authenticate_with_api_key",
  "docsUrl": "https://polytrackers.com/docs/api"
}

Valid Authorization: Bearer ptk_… requests bypass BotID entirely — see llms.txt.


Quickstart (agent flow)

1) Sign in and obtain a session

PolyTrackers auth is passwordless. Create an account or sign in through the web app at /register or /login using Google OAuth or an emailed magic link (Terms of Service and Privacy Policy acceptance is captured during sign-up). This yields a Supabase session — a session cookie plus an access_token / refresh_token pair you can pass as a bearer token.

Exchange a refresh token for a fresh session pair when the access token nears expiry:

bash
curl -X POST "https://polytrackers.com/api/auth/refresh" \
  -H "Content-Type: application/json" \
  -d '{"refresh_token":"'"$REFRESH_TOKEN"'"}'

Response includes:

  • access_token
  • refresh_token
  • token_type
  • expires_in

2) Read account + tier

bash
curl "https://polytrackers.com/api/account" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Returns tier (effective tier, honoring active trial), trialEndsAt, apiKeysCount, walletsCount, onboarding + wallet-link status.

3) Discover markets

bash
curl "https://polytrackers.com/api/markets?query=btc&status=open&sort=volume&limit=25"

To decorate public market cards with coarse recent signal evidence, pass up to 100 comma-separated condition IDs. The response contains only 24-hour whale and anomaly booleans—no wallet, trade-size, price, or execution data:

bash
curl "https://polytrackers.com/api/market-signals?conditionIds=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

For one market or event, the anonymous detail endpoints return more than the list rows do. GET /api/markets/{slug} includes the full resolution-rules description plus a heuristic resolutionRisk score; GET /api/events/{slug} returns the event's complete outcome ladder (every priced leg, not just the legs on a paginated list page); GET /api/events/featured returns the current featured multi-outcome event, or {"event": null} when none qualifies:

bash
curl "https://polytrackers.com/api/markets/some-market-slug"
curl "https://polytrackers.com/api/events/some-event-slug"
curl "https://polytrackers.com/api/events/featured"

Anonymous requests to /api/markets, /api/events, /api/home-snapshot, and /api/market-signals share one 60 requests/minute per IP discovery budget. Pace discovery loops across the group and honor Retry-After on 429. The {slug} detail endpoints are metered separately: each has its own 60 requests/minute per IP bucket, and both can return 503 with reason: "upstream_request_budget_deferred" when the shared upstream request budget defers the fetch — honor retryAfterMs before retrying.

4) Trigger anomaly scan (Pro/Elite)

bash
curl -X POST "https://polytrackers.com/api/scan/trigger" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

5) Read anomalies

Session, or a Pro+ Agent API Key (signals:read):

bash
curl "https://polytrackers.com/api/anomalies?limit=50&severity=high" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Free-tier session callers see at most 5 rows, within their retention window (7-day base, up to 28 days with activated referral rewards), and only those detected more than 1 hour ago. Direct REST access with an Agent API Key requires Pro+.

6) Read anomaly detector performance

Aggregate win-rate, average return, and P&L for historical anomaly detections. Backed by the anomaly_performance_daily view.

bash
curl "https://polytrackers.com/api/anomalies/performance?since=2026-01-01T00:00:00Z&until=2026-04-01T00:00:00Z&group_by=month&anomaly_type=SHARP_MOVE" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Query parameters:

  • since, until — ISO-8601 datetimes. Defaults to the last 30 days; the window is capped at 365 days.
  • group_by — one of day (default), month, anomaly_type, severity, detector_version, category, all.
  • anomaly_type, severity — optional filters passed straight through to the view.
  • detector_version — optional legacy detector label (for example v2). When omitted, Anomaly 2.0 (v3) receipts are excluded from the blended population; passing v3 returns 400 because that cohort is not available on this public endpoint.
  • category — optional market category: sports, oil-energy, politics-elections, crypto, economics, or other.

Response body:

  • window — { since, until } echoing the resolved window (ISO-8601).
  • totals — aggregate { flagged, resolved, wins, losses, voids, winRate, avgReturnPct, totalPnlUsd, equalDollarPnlPer100 } across the window.
  • series — same shape as totals but keyed by cohort (the group_by bucket), sorted ascending.
  • coverage — resolved / flagged for the window (0–1).
  • coverageThreshold — the coverage at which the cohort is considered mature enough to surface headline numbers (currently 0.5).
  • minDecidedForHeadline — the absolute number of decided outcomes (wins + losses) that is, on its own, enough to surface headline numbers (currently 20).
  • headlineReliable — coverage >= coverageThreshold || (wins + losses) >= minDecidedForHeadline. Because markets resolve long after detection, a window can have low coverage while still holding a large, meaningful sample of decided outcomes; either condition makes the win rate reliable.

equalDollarPnlPer100 is a simulation: $100 flat-staked on every decided anomaly in the cohort, expressed as total dollars of P&L.

Accepts session auth (cookie/JWT) or a Pro+ Agent API Key with signals:read scope. Responses are cached privately with s-maxage=60, stale-while-revalidate=300.

7) Read unified trade history

bash
curl "https://polytrackers.com/api/trades?type=all&status=all&sort=newest&limit=25&offset=0" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response body includes tier and historyDepthDays; the latter is the mock history window (7 for Free, 90 for Pro, null for Elite). Own real-trade receipts are full-depth at every tier. For real rows, status describes the position: a filled BUY remains open until resolution or exit accounting is recorded. rawStatus keeps the order lifecycle, and closedAt is only an actual resolution/settlement time—never a bookkeeping updated_at value. Real rows also carry failureReason and failureDetail explaining why a failed/cancelled attempt terminated; both are null on healthy rows. fillAmount, requestedShares, and price expose the durable execution facts; isDustFill is derived when filled shares are below the applicable market share minimum, executed notional is below $1, or the fill is under 10% of requested shares. Execution reuses live market/book metadata; bounded list and metric reads use the centralized conservative fallback and make no per-row provider calls. Dust remains real P&L but is excluded from win-rate and mock↔real parity grading. payoutReconciliationStatus carries the receipt-backed payout state, while modeledPayout is the deterministic binary/SPLIT estimate used during reconciliation and must not be presented as observed spendable cash.

A type=real&status=executed response also includes realWalletStats. That object is aggregated server-side over every matching executed real-trade row, independent of limit/offset, and reports realized P&L, all-time decided-BUY cost basis/ROI inputs, open positions, and resolved rows still awaiting P&L attribution. The trades array remains paginated.

conditionId narrows the feed to one exact Polymarket condition ID. Offset pagination is reliable only before offset 1000 because PostgREST caps each source window. A page that reaches that boundary returns its reachable prefix as an explicit terminal page with pagination.depthLimited: true, depthLimit: 1000, and depthLimitReason: "postgrest_max_rows"; narrow by type, status, wallet, or condition instead of continuing.

When the wallet-scope integrity tripwire drops a foreign raw row, the returned trade rows remain verified against the requested wallet but count metadata cannot be proven from the same suspect result set. The response therefore adds pagination.degraded: true, degradedReason: "wallet_scope_invariant_violated", degradedFields: ["total", "hasMore"], and droppedRows. Treat total and hasMore as untrusted for that incident response; normal-path pagination is unchanged.

Direct REST access with an Agent API Key requires signals:read plus current Pro or Elite access; a Free key gets 401. The 7-day Free history depth applies to session callers. Free-tier agents use MCP pt_trades_list (signals:read, Free) for the same feed.

8) Run a historical backtest

Free Agent API Keys can run the fixed demo configuration with signals:read; the returned trade list is capped at 5 rows. Pro and Elite keys can customize the configuration within their tier's window limits. costModel: "leader" (default) uses the request's flat slippageBps and feeBps. For a whale-copy admission run, use costModel: "copy": the server applies the daily copy-receipt slippage bucket for each entry and charges the measured entry-fee rate on every trade. The response labels the calibration source and timestamp; missing receipt evidence falls back to documented non-zero conservative costs.

bash
curl -X POST "https://polytrackers.com/api/backtest" \
  -H "Authorization: Bearer $AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source":{"kind":"anomalies","minSeverity":"HIGH"},
    "stake":{"mode":"flat","amountUsd":100},
    "startingBankrollUsd":1000,
    "windowDays":30,
    "costModel":"leader",
    "slippageBps":50,
    "feeBps":0
  }'

The copy-actionability gate recommends the same shape with a whale source, windowDays 90 or 180, and costModel: "copy". Its old leader-cost runs do not satisfy the gate. A canonical whale run returns outOfSampleValidation: the requested window is split into a pre-holdout selection slice and an independently replayed trailing 30-day holdout. Both slices must enter at least one trade, avoid bankroll bust, and have positive copy-cost capital-accounting ROI. The full requested-window result remains reporting evidence, but it cannot clear admission when either disjoint slice fails; leaderboard and recommendation actionability then expose OUT_OF_SAMPLE_VALIDATION_FAILED.

Every REST result includes strategy.stats_by_entry_price_bucket. It always contains the five copy-calibration entry-odds buckets (<0.10, 0.10-0.35, 0.35-0.65, 0.65-0.90, >=0.90), including explicit zero-trade rows. Each row reports trades, wins, losses, splits, entryCostUsd, totalPnlUsd, capital-weighted roiPct, and arithmetic avgReturnPct. The aggregation is computed from the existing settled trade log and adds no query or stored data.

9) Readiness-gated real trade execution

Session auth requires manual trading readiness: trading wallet/signing setup, wallet funding, wallet risk config, circuit-breaker state, idempotency, reservations, region certification, and audit logging.

Session auth:

bash
curl -X POST "https://polytrackers.com/api/trade/execute" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "conditionId":"0xconditionid",
    "side":"YES",
    "amount":20,
    "limitPrice":0.5,
    "idempotencyKey":"agent-trade-2026-04-16-001",
    "regionCertification":true
  }'

Agent API Key with trade:execute scope:

bash
curl -X POST "https://polytrackers.com/api/trade/execute" \
  -H "Authorization: Bearer $AGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c2b7f9e-47b8-4ad2-95b5-8f8d5f2f6a3c" \
  -d '{"conditionId":"0x...","side":"YES","amount":20,"regionCertification":true}'

CDP delegated-wallet browser users must submit explicit limit orders until the SDK adapter supports cap-priced immediate-or-cancel market orders. Non-CDP market buys retain the server-side slippage-cap path.

10) Cancel an own resting real order

Session callers can cancel their own resting order even when their tier cannot list all CLOB orders:

bash
curl -X DELETE "https://polytrackers.com/api/trade/orders/$ORDER_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The two programmatic paths have different gates: direct REST bearer calls to GET /api/trade/history, GET /api/trade/ledger, GET /api/trade/money-summary, and GET /api/trade/orders* require agent:full. MCP pt_trade_history_get requires signals:read at every tier, while pt_trade_orders_list and pt_trade_orders_get require signals:read plus the Elite tier.


Agent API Key lifecycle

Generate Agent API Key (JWT bearer only)

Free callers can generate read-only Agent API Keys for MCP access; requested scopes are reduced to signals:read and MCP usage is capped at 200 requests/day. Pro callers may request signals:read, the narrow trade:execute scope, and the narrow agent:scan scope; Elite callers may additionally request agent:full. Accounts keep 1 active Agent API Key; rotate by revoking the old key, then generating a replacement and updating clients and agents (409 API_KEY_LIMIT_REACHED when the cap is reached).

bash
curl -X POST "https://polytrackers.com/api/keys/generate" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"nickname":"Agent API Key","expiresInDays":90,"scopes":["signals:read"]}'
  • expiresInDays: 1–365, defaults to 90.
  • scopes: defaults to ["signals:read"]. Free keys are always reduced to signals:read; paid-tier validation still applies outside the free MCP/read surface. Add trade:execute only when intentionally creating a real-trading key. Pro-tier requests for agent:full are stripped, while trade:execute and agent:scan are allowed where their automation surfaces are enabled.
  • The raw api_key is returned once in the response — store it securely.

List keys

bash
curl "https://polytrackers.com/api/keys" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Use Agent API Key for public whale feed (Elite)

bash
curl "https://polytrackers.com/api/public/whale-signals?limit=25" \
  -H "Authorization: Bearer $AGENT_API_KEY"

Outbound webhooks (Pro/Elite)

Create, list, delete, and test delivery endpoints live under /api/webhooks*:

  • GET /api/webhooks — list your registrations.
  • POST /api/webhooks — register a new URL with optional filters (minScore, severity[], anomalyTypes[], marketCategories[]) and a secret. Capped at 5 (Pro) / 50 (Elite); over-limit returns WEBHOOK_LIMIT_REACHED.
  • DELETE /api/webhooks/{id} — soft-delete a registration. The row is retained for audit (with deleted_at stamped and status=paused), it will stop receiving future deliveries, and historical delivery_log entries remain queryable.
  • GET /api/webhooks/{id}/deliveries — recent delivery logs.
  • POST /api/webhooks/{id}/test — fire a test payload.

Webhook delivery contract

Matching anomaly alerts are delivered as anomaly.detected JSON:

json
{
  "event": "anomaly.detected",
  "timestamp": "2026-07-20T14:30:00.000Z",
  "anomaly": {
    "id": "anomaly-id",
    "score": 82,
    "market_title": "Will BTC reach $150k in 2026?",
    "market_slug": "will-btc-reach-150k-in-2026",
    "market_url": "https://polymarket.com/event/will-btc-reach-150k-in-2026",
    "current_price": 0.64,
    "anomaly_type": "WHALE_ANOMALY",
    "severity": "HIGH",
    "side": "BUY",
    "outcome": "YES",
    "bet_recommendation": {
      "recommendedBet": 100
    },
    "whale_flag": true
  }
}
  • Delivery targets must be public HTTPS URLs. Redirects are treated as failures and are not followed.
  • Any 2xx response completes delivery. A failed initial attempt is retried after 30 seconds, then 5 minutes, then 30 minutes: 4 total delivery attempts.
  • Each registration is capped at 100 delivery attempts per rolling hour, including retries.
  • When the registration has a secret, verify X-Webhook-Signature against sha256=<HMAC_SHA256(raw_body, secret)> before parsing the JSON.

X-Webhook-Signature signs outbound user deliveries. It is distinct from the inbound x-alchemy-signature required by /api/whale/webhook.


Mock-wallet unrealized P&L freshness

GET /api/wallets exposes mock-wallet unrealizedPnl as a persisted point-in-time snapshot, not a continuously priced equity value. unrealizedPnlMarkedAt records the pricing-pass timestamp; null means no trustworthy mark timestamp exists. The companion unrealizedPnlIsSnapshot, unrealizedPnlCaveat, and unrealizedPnlAuthority fields keep this contract machine-readable.

For current equity decisions, use pt_mock_analytics_get.wallet_mark_to_market.preferred_unrealized_pnl and inspect live_pricing_status. A complete analytics pricing pass refreshes the persisted snapshot after the read returns. Partial or unavailable pricing never overwrites the last complete mark. A partial pass covering at least 90% of open positions nevertheless becomes the preferred live value, with unpriced legs held at cost basis; lower coverage falls back to the timestamped snapshot.

The reserved real-money-copy row is never accepted by mock analytics and never receives its write-behind. In normalized /api/wallets and GET|POST /api/trade/real-wallet responses, its mark timestamp is null and its provenance instead points to real_trades for position P&L and /api/trade/wallet-funds for spendable funds.

GET /api/wallets derives current-day realized P&L from get_wallet_daily_copy_stats. dailyPnl and the backward-compatible daily_pnl alias always carry the same UTC-day value. If the projection fails, both are null and dailyPnlDegraded:true / dailyPnlStatus:"degraded" make the partial response explicit; the route never substitutes the denormalized mock-wallet counter. Reserved real-wallet legacy performance aliases are projected from real_trades. A reserved real wallet whose risk configuration is missing or does not select the real lane makes the daily projection fail closed instead of reading mock_trades.

GET /api/trade/wallet-funds also returns equityIdentity for the authenticated real wallet. Its complete identity is:

supportedStablecoinValue + openMarkedValue + pendingRedemptionValue - netDeposits = lifetimePnl

supportedStablecoinValue sums the selected funding wallet's pUSD, native USDC and USDC.e at the ledger's nominal $1-per-token basis. spendablePusd remains pUSD only; unconvertedStablecoinValue is USDC plus USDC.e, which require conversion before trading. The sum already includes spendable pUSD: do not add it again. Token conversions are not new capital or profit. These three token reads bypass caches and share one Polygon block. Invalid, missing, cached, wrong-wallet or older-than-60-second token evidence returns a degraded identity with null stablecoin totals and lifetime P&L. A failed chain snapshot returns 502, not a verified zero. This endpoint uses nominal parity, not market depeg quotes. Account ROI is lifetime P&L / positive net deposits, distinct from cumulative trade-efficiency ROI.

Capital still comes from the existing user-wide ledger and holdings from the selected funding address; historical multi-wallet capital allocation remains unresolved. The token snapshot does not synchronize the venue positions or ledger, so in-flight transfers/redemption indexing can temporarily differ.

pendingRedemptionValue comes from the bounded Polymarket Data API positions read and includes only positions marked redeemable. The separate modeledPendingRedemptionValue is the unresolved-payout-row projection for operations diagnostics only: it never enters lifetimePnl, and its absence alone does not degrade the identity. netDeposits is the signed sum of deposit and withdrawal ledger entries, while openMarkedValue excludes redeemable venue positions so pending winnings are not double-counted. If the positions read or another required component is unavailable, status: "degraded" and lifetimePnl: null fail closed; callers must not reconstruct or display a partial lifetime P&L as complete.


Deletion & audit retention

PolyTrackers favors soft deletion for user-owned resources so that reporting, billing, copy-trading analytics, and security audits remain reliable. The behavior per-resource is:

ResourceEndpoint(s)Behavior
Agent API KeysDELETE /api/keys?id=…Soft-revoke: is_active=false, revoked_at=now(). Authentication is rejected immediately; row is retained for audit.
Mock walletsDELETE /api/wallets, DELETE /api/mock/wallet/{id}Soft-delete: is_active=false, deleted_at=now(). Whale assignments are detached (active=false, removed_at=now()). mock_trades, whale_activity, and whale_decision_traces are preserved. Wallet names can be reused.
Outbound webhook registrationsDELETE /api/webhooks/{id}Soft-delete: deleted_at=now(), status=paused. No future deliveries fire; historical delivery_log remains queryable via GET /api/webhooks/{id}/deliveries.
Alert preferencesDELETE /api/alerts/preferencesHard reset of the preferences row.
Roster (whale) assignmentsDELETE /api/roster/{address}Detach: row retained, active=false, removed_at=now().
Trade ordersDELETE /api/trade/orders/{id}Cancels the live order at the CLOB; hard-removes the queued row.

Mock trades are not user-deletable by design once placed — open, closed, and resolved rows feed the leaderboard, copy-trading scoring, and tier-history analytics. To exit an open mock position, close it through POST /api/mock/resolve; this records the terminal state and preserves the trade row for history.


Whale service + inbound webhooks

Existing whale-service endpoints remain supported and documented in OpenAPI:

  • Wallet/roster management (/api/wallets, /api/roster*)
  • Service status/config/control (/api/whale/*)
  • Webhook ingest/events/replay (/api/whale/webhook*)

For replay automation, use dryRun: true first, then replay live once validated.


Discovery endpoints

Anonymous, CDN-cached, and safe to fetch before you have a key. Every one of them describes a surface documented in this file:

PathMedia typeWhat it is
/openapi.jsonapplication/jsonThe OpenAPI 3.1 contract for the REST API. /api/openapi.json is a 301 alias.
/.well-known/api-catalogapplication/linkset+jsonRFC 9727 API catalog: a linkset naming the REST, MCP, and documentation MCP entry points, each with its service-desc (machine-readable description), service-doc (human documentation), and service-meta (metadata) links. The one URL to fetch when you want the developer resources and nothing else.
/.well-known/mcpapplication/jsonMCP handshake manifest (alias /.well-known/mcp.json).
/llms.txttext/plainAgent instructions: when to use PolyTrackers, automation-safe lanes, error shapes, and the crawl policy.
/skill.mdtext/markdownThe official agent skill — tool catalog, scopes, and workflows.

MCP server

PolyTrackers exposes a stateless MCP endpoint for agent clients:

  • GET /api/mcp — unauthenticated health/capability probe.
  • GET /.well-known/mcp (alias GET /.well-known/mcp.json) — unauthenticated manifest describing this server: transport URL, advertised protocol versions, capabilities, auth scheme, and the stdio bridge package. Generated from the same constants the server itself uses, so it cannot drift.
  • POST /api/mcp — JSON-RPC 2.0 over Streamable HTTP. Send Authorization: Bearer <agent-api-key> — use the same ptk_... key as your REST/API examples.
  • GET /api/mcp/events — optional authenticated SSE notifications for anomalies, whale_signals, copy_signals, and scan_progress, with : keepalive every 15s. Send Authorization: Bearer ptk_.... The whale_signals and copy_signals topics are Elite-only; requests for them from Free or Pro keys are silently dropped while the 200 stream remains live for permitted topics. Optional severity (comma-separated, case-insensitive) and market_slug (exact match) query parameters narrow delivery to events whose payload carries a matching field; events without that field are dropped.
  • GET / POST /api/mcp/docs — the anonymous documentation MCP server: a separate, read-only Streamable HTTP server that takes no API key and reads no Authorization header. GET is a CDN-cacheable health probe (it reports authentication: "none"); POST is stateless JSON-RPC serving three read-only tools — docs_list, docs_search, docs_get — plus the same documents as MCP resources, over the published .md page twins and section llms.txt indexes. It serves no live market, whale, or account data. Both operations are in the OpenAPI contract at /openapi.json; the pre-connection card is /.well-known/mcp/docs-server-card.json.

SSE connections last at most 800 seconds (~13 minutes), so reconnect when the server closes the stream. There is no Last-Event-ID resumption: each reconnect starts at the current Redis position ($), so events published while disconnected are permanently missed.

When the client supports Streamable HTTP, point it at https://polytrackers.com/api/mcp with Authorization: Bearer <agent-api-key>.

For stdio-only MCP hosts, run the published bridge with npx -y @polytrackers/mcp-stdio; it relays stdio JSON-RPC to that endpoint. It reads POLYTRACKERS_API_KEY (required), POLYTRACKERS_MCP_URL (optional, defaults to the endpoint above), POLYTRACKERS_MCP_ALLOWED_HOSTS (optional host allowlist), and POLYTRACKERS_MCP_TIMEOUT_MS (optional). Full hosted and stdio setup examples are published in /skill.md.

To test strategies programmatically with no real money — the mock (paper) trading tool flow, free-tier reads, and going-live fees — see the Polymarket Mock Trading API guide.

Agent skill distribution

Install the PolyTrackers agent skill (which bundles this MCP setup) with one command: npx skills add polytrackers/polymarket-copy-trading-skill. Official distribution channels:

Scopes:

  • signals:read — read-only tool/resource catalog for Free+; free keys are shaped by 7-day base anomaly history (up to 28 days with activated referral rewards), top-5 delayed anomalies, and the 200 requests/day MCP quota.
  • trade:execute — permits pt_trade_execute for Pro+ after pt_trade_preflight approval.
  • agent:scan — permits pt_scan_trigger for Pro+ without granting broader writes.
  • agent:full — current Elite write/execution superset.

Versioning & changes

  • 2026-10-14: Paper and real-copy wallet risk config now exposes smallOrderPolicy (scale-up by default, or skip) through PUT /api/whale/config and both guarded MCP config writers, and new wallets default minTradeSize to $1. The minimum copy size is the larger of $1, the enabled minTradeSize, and (real wallets) the signed minPerTrade; it applies after fixed or percentage sizing and before final risk validation and reservation. In scale-up mode a positive copy below the minimum is raised to it when every risk, authorization, balance, fee, and venue gate allows, which can exceed the strategy-sized cost; otherwise it is skipped with a reason. Real scaling still needs a signed minPerTrade: null/0 authorizations keep skip behavior until the owner changes them, and stored custom minimums are retained. skip never enlarges a copy, and SELL quantities and actual partial fills are never enlarged. This supersedes the 2026-08-10 minPerTrade note below.

  • 2026-10-01: Real-wallet copy controls now reflect mandatory authorization pauses. Expired signing or inactive copy authorization returns fully_paused, an actionable authorizationPause, and the preserved savedMode. Resume attempts return 409 until signing and copy authorization are restored. Paper wallets and whale assignments are unchanged; an existing manual pause still applies after reauthorization.

The current REST/OpenAPI contract version is 2.0.0, sourced from the canonical OpenAPI document and served at /openapi.json. The hosted MCP server version is 0.2.0, sourced from the server package version and returned by the MCP initialize response and each tool response's _meta.server_version. Additive endpoints and tools may ship without a version bump.

Version header

The REST contract is versioned in a response header, not in the URL path. Every /api/* response carries:

HeaderMeaning
API-VersionContract version that produced this response, e.g. 2.0.0. Matches info.version in the OpenAPI document.

Assert on it at startup and fail loudly on an unexpected major, rather than discovering a change through a parse error later. The header is response-only: no request header changes how a route behaves, so there is nothing here that can be pinned to and later withdrawn. Paths outside the proxy's matcher (/api/cron/*, /api/whale/webhook, /api/goldsky/shadow) are machine-to-machine and omit it.

The OpenAPI document declares this header on every response it describes, as $ref: "#/components/headers/ApiVersion" — so a generated client sees it without reading this page, and a spec parser can detect the versioning strategy. The four POST /api/whale/webhook responses deliberately carry no such declaration, because that path is outside the matcher. A contract test holds both halves in place against the matcher in proxy.ts.

How the number moves:

  • Patch/minor — additive only. New endpoints, new optional request fields, and new response fields ship without a major bump, so clients must ignore response fields they do not recognise.
  • Major — anything that could break a correct client: a removed or renamed field, a narrowed type, a changed status code, or a retired endpoint. A major is always preceded by the deprecation signal below.

Deprecation and sunset headers

A retired endpoint announces itself on the wire, not only in this document:

HeaderMeaning
DeprecationRFC 9745 structured-field date — @<epoch seconds> — for when the deprecation was announced.
SunsetRFC 8594 HTTP-date for when the endpoint stops serving. A date in the past means it is already gone.
Linkrel="successor-version" pointing at the replacement, plus rel="deprecation" pointing at this page.

POST /api/whale/trade is the current example: retired on 2026-02-27, it answers 410 Gone and carries all three headers pointing at POST /api/whale/webhook. Poll Deprecation on the endpoints you depend on — it is the earliest machine-readable warning available, and it is set long before the Sunset date arrives.

Compatibility and deprecation policy:

  • Breaking REST contract changes are recorded in this section when they ship.
  • MCP tool additions, removals, and renames are recorded in the polytrackers://changelog resource. Its current catalog count and fingerprint are checked against the registered catalog in CI.
  • Breaking MCP changes bump the server minor version. A renamed tool remains available for one minor version with deprecated: true and a replacement in its description before removal.
  • The MCP HTTP transport does not emit a Deprecation response header for tool changes; the REST headers above cover REST endpoints only. For MCP, consult this section and polytrackers://changelog.

Change log:

  • 2026-09-11: Agent alert-preference updates now redact stored Discord webhook tokens from their response. pt_mock_experiment_run no longer accepts the ineffective fund_amount input and uses the caller tier's standard balance when it creates a wallet. Webhook registration rejects URL credentials and custom HTTPS ports, while POST /api/wallets bounds its body to 64 KiB and enforces the established 100-character name and 20-character color limits.

  • 2026-10-12: Wallet-risk REST and mock/real MCP config expose excludeShortHorizonMarkets (strict boolean, default false). When enabled, BUYs within 24 hours are skipped across all categories with MIRROR_SKIPPED_SHORT_HORIZON; sports use game/event start and non-sports use market resolution time. Recognized short-window crypto/daily-price markets with unknown timing also skip. SELLs are unaffected. Paper atomic execution repeats the check; config import to a real wallet preserves the policy.

  • 2026-09-28: Wallet-risk config, REST, and mock/real MCP tools expose an optional inclusive copied-BUY entry band (minEntryPrice 0 by default, maxEntryPrice 1 by default) and excludeSameDaySports (false by default). The latter skips sports-like markets ending within 24 hours or with an unknown end date. Mock fills check final VWAP atomically; real BUYs check the observed executable price before reservation because final fill VWAP is unavailable there. SELL exits remain unchanged.

  • 2026-09-08: Wallet-risk updates now apply the existing $10,000,000 absolute ceiling to hourlyLossLimit and dailyLossLimit across the dashboard, REST endpoint, and both guarded MCP config writers. Unexpected PUT /api/whale/config failures retain full server-side logs but return a fixed message instead of exposing raw infrastructure details.

  • 2026-09-07: The Elite read-only pt_real_wallet_copy_config_get projection now includes effective signed copy-trading authorizationLimits.maxPerTrade and authorizationLimits.dailyLossLimit. Null means a ceiling is unavailable; both remain distinct from same-named wallet-risk fields under config, and no write path or signing material was added.

  • 2026-08-23: The OpenAPI document now declares API-Version on every response it describes, instead of only defining the header component and describing it in prose. Documentation only — the header itself has shipped on the wire since earlier the same day, and no endpoint's status, body, or media type changed.

  • 2026-08-23: Every /api/* response now carries API-Version, and the retired POST /api/whale/trade additionally carries Deprecation, Sunset, and a successor Link. Additive headers only — no status code, body, or cache directive changed, and page routes are untouched. Every OpenAPI operation also now has a description; a contract test fails the build if one ships without.

  • 2026-08-14: Wallet copy pause is now a three-state REST/MCP contract: copying, buys_paused, or fully_paused. Buys-only mode blocks new BUY mirroring with WALLET_COPY_BUYS_PAUSED while paper and real SELL exits keep flowing. The v2 MCP setter prefers mode and retains legacy copyPaused:true|false; every live transition remains atomically audited, with explicit confirmation and a best-effort security email for real wallets.

  • 2026-08-14: Added the operator-allowlisted, Elite, read-only, MCP-only pt_copy_replication_aggregates_get internal evidence tool over the service-role Copy Receipt V1 aggregate RPC. It requires one exact whale, fixes the RPC limit at one, and adds index-backed lookup plus a caller-side five-second PostgREST request deadline. Database-owned 20/40-condition confidence floors and below-floor null metrics are unchanged; ordinary customer keys fail closed and no REST or browser surface was added.

  • 2026-08-18: Paper and dedicated real-copy wallets now expose fixedStakeUsd. Zero preserves conviction/percentage sizing; a positive value of at least $1 requests the same USD amount for every copied BUY and bypasses the copy multiplier. Kelly, position, max-bet, balance, authorization, breaker, reservation, and venue gates remain authoritative. The dashboard plus both guarded MCP config writers can read, preview, and update the wallet-scoped field; existing wallets default to off.

  • 2026-09-07: Paper and real-copy risk config now exposes default-on nettingProtection and opposingSideProtection toggles. Owners may opt out per wallet. Disabled netting activates maxConditionExposureUsd, which caps total open BUY cost across both outcomes plus the proposed copy, defaults to three times maxPerTrade, stays at or above that per-trade limit, and is bounded to $10,000,000. Real protection opt-outs remain atomically audited and emit copy_protection_disabled.

  • 2026-08-11: Added the free read-only MCP tool pt_whale_forward_returns_get for bounded forward-return counterfactuals across arbitrary wallet cohorts. It preserves one row per request entry, enforces exact decision/cutoff windows, and exposes unresolved, truncation, fee, resolution-source, and vendor-call-budget evidence.

  • 2026-08-10: Dedicated real-copy wallets now persist a guarded 0–10% realCopySlippageTolerance, exposed through the real-wallet MCP config tools. Where the venue ceiling permits, BUY FAK caps retain at least one market tick of headroom above the observed ask, and deterministic signed-cap rejections are no longer replayed through Goldsky promotion recovery.

  • 2026-08-10: Recommendation and digest-suggestion rows now expose display-name source/time plus Polymarket's stable pseudonym. Legacy aliases beginning with a wallet address no longer override the current leaderboard name, and compact/byte-capped/cursor-replayed shapes retain the provenance.

  • 2026-08-07: Rollout-eligible dedicated real-copy wallets can now set a 1–2x owner-only sizing multiplier through the authenticated dashboard. The server gate fails closed to 1x, all existing caps remain authoritative, and MCP real-wallet writes continue to exclude the field.

  • 2026-08-10: Real-wallet dashboard and Elite MCP config surfaces now read, preview, and write authorization-scoped minPerTrade. Null/0 disables the floor; positive values are bounded to $1 through the signed copy-trading authorization's maxPerTrade, restore only percentage-reduced dust, and never exceed the strategy-sized cost. The floor applies only when fixedStakeUsd is off and is explicitly distinct from risk-rule maxPerTrade and minTradeSize. Superseded on 2026-10-14: the floor now applies after fixed or percentage sizing and can exceed the strategy-sized cost when smallOrderPolicy is scale-up (see that entry above).

  • 2026-08-07: Session-authenticated PUT /api/whale/config now routes real-wallet risk changes through the atomic config-plus-audit RPC and sends the owner security email. Real-wallet requests reject executionMode; paper-wallet behavior is unchanged.

  • 2026-08-07: Whale-edge loads are now batch-independent under deterministic per-wallet caps. coverageTruncated is wallet-specific, SPLIT/non-directional evidence is exposed through nonDirectionalSampleCount*, and roi_provenance includes the capital used by each ROI window.

  • 2026-08-07: Leaderboard, recommendation, and digest-suggestion rows now share canonical ROI/sample projections and a roi_provenance block across full, compact, and replayed snapshot shapes. Bare sampleCount means the directional edge-row count; recommendation admission evidence is separately named. Explicit edge-load fallback uses labeled 365-day receipts-only counts with null ROI.

  • 2026-08-04: pt_recommendations_get now returns snapshot-pinned opaque continuation cursors plus explicit total/returned/has-more counts. Continue with the same projection and settled-sample filter when the byte cap trims a page; min_settled_samples narrows the population and is not continuation.

  • 2026-08-03: Added Elite agent:full pt_real_wallet_copy_config_get / pt_real_wallet_copy_config_update and pt_real_wallet_breaker_resume / pt_real_wallet_breaker_kill. Every real-money preview or write requires confirm_real_money:true; live changes append an audit event and send a best-effort security email. The strict config writer excludes executionMode and copySizingMultiplier.

  • 2026-07-31: Added Elite agent:full pt_mock_wallet_copy_config_get / pt_mock_wallet_copy_config_update for full caller-owned paper-wallet risk reads and guarded dry-run/live updates. The strict write schema excludes executionMode, real-wallet rows are unreachable, and zero-value warnings are returned with the preview.

  • 2026-07-26: Added GET /api/trade/money-summary (user-scoped real-money totals/counts aggregate; session or agent:full, same auth semantics as GET /api/trade/ledger).

  • 2026-07-21: Added the paper-only wallet copy-sizing multiplier to the wallet risk-config contract and pt_mock_wallet_copy_sizing_update; the MCP catalog baseline was then 65 tools and real-copy sizing was 1x. The 2026-08-07 owner-dashboard 1–2x rollout supersedes that historical behavior.

  • 2026-07-20: Published the version/deprecation policy and recorded the 2.0.0 OpenAPI contract plus the then-current 64-tool MCP catalog baseline.


Reliability notes

  • Handle 429 on all rate-limited endpoints and respect Retry-After.
  • Treat trade execution as idempotent when you provide idempotencyKey (idempotent replays return 200 with idempotencyReused: true).
  • POST /api/keys/generate returns the raw key once — store it securely. Each account keeps one active Agent API Key; rotate by revoking the old key, then generating a replacement and updating clients/agents.
  • /api/anomalies/{id} may return 404 when a record falls outside your tier window.
  • TIER_UPGRADE_REQUIRED responses include requiredTier, currentTier, and upgradeUrl — surface these to the user/agent verbatim.

Historical sports durability compares trade time with event timing, independently of query age. Missing/conflicting timing stays unknown. Discovery actionability recent_activity.durability_evidence and digest wallet durability_profile.durability_evidence retain timing-basis counts, known-notional coverage and shares, majority basis, and the capped loaded window through compact and cursor responses. Per-trade activity_profile retains timing basis and uncertainty. See historical flow durability for the 24-hour horizon, conservative slug calendar envelope, 90% notional-coverage rule, 1,000-row disclosure, and denominator semantics. No new execution veto is added.

Leaderboard backtest population and market-specific discovery

backtestable_only vets usable coverage within the returned Polymarket leaderboard population. It narrows discovery; missing or unavailable coverage is not a negative profitability or category verdict. A new or untracked wallet can lack evidence even when it has useful trades. This filter is separate from the canonical capital-accounting backtest admission gate.

Resolved-position scope enrolls the top 50 leaderboard wallets selected by whale edge plus active wallet_whale_assignments roster wallets. Ingestion requires copyable classification; high-fill-rate market makers are excluded from that lane. Bounded Data API backfills add historical fills for scoped wallets, and coverage/backtests union those positions with legacy resolved_receipts joined by anomaly_logs.wallet, deduplicating conditions. Missing resolved-position coverage retains receipt-based diagnostics; inspect per-row backtestCoverage, roi_provenance, truncation and fallback fields. Enrollment alone guarantees neither complete history nor current freshness.

REST /api/leaderboard and MCP pt_leaderboard_get return the explanation in omission_metadata.backtestable_scope with either filter setting and in full, compact and continued responses. Existing omission counts and continuation semantics remain unchanged. Unknown coverage does not become proof of poor performance or an absence of durable-category traders.

For market-specific discovery, obtain a condition id with pt_markets_search, then call Pro+ pt_market_trades_get to find recent public trading wallets. That tool reads at most 100 source rows, has no cursor, and reports omissions; it does not enroll wallets or create backtest evidence. Follow candidates with pt_trader_profile_get for coverage and pt_whale_activity_get for ingestion/timing evidence, then pt_backtest_run for modeled copy-cost validation. One market sample guarantees neither durable flow nor profitable whales, and a passing backtest remains screening evidence rather than proof of live copy profitability.

Roster session POST/PATCH/DELETE requests enforce the configured site origin and reject cross-site browser writes; adding an arbitrary bearer header cannot bypass this check. Verified whale-service secrets keep their programmatic access, and scoped Agent API Keys keep the MCP roster tools. REST roster-add name accepts a string of at most 100 UTF-16 code units before normal display name sanitization, matching MCP's length contract. Unexpected roster database errors return fixed operation-specific messages while server logs retain the underlying failure.

Session writes to PATCH /api/whale/copy-pause, PUT /api/whale/config, and POST/PATCH /api/trade/copy-trading enforce the configured site origin before authentication, body parsing, or mutation. Cross-site fetch metadata or a mismatching Origin returns HTTP 403 with { "error": "Cross-origin requests are not allowed." }. Adding a bearer header does not bypass the origin guard. Headerless clients retain existing authentication requirements, and scoped Agent API Key MCP tools are unchanged.

Staged Data API v2 bet sync adds optional historyComplete to POST /api/bets/sync and its bets_sync_trigger MCP projection. false means successful sync of a bounded recent sample, not complete wallet history. A failed source is an error, not an empty/up-to-date result. Selection uses the existing server-only history rollout cohort; this contract does not activate it.