TailSlip
Log in

Command Palette

Search for a command to run...

On this page

TailSlip API

TailSlip tracks the best traders on Polymarket and shows where they agree. This document describes the public REST API that the web app uses. You can call the same API from scripts and bots.

Analytics only. Nothing the API returns is investment advice. The portfolio model and the paper follower are experimental and unvalidated. Polymarket restricts trading in some places, including the US.


1. Basics

Base URL

https://tailslip.com/api

The Next.js frontend forwards every /api/* request to the Go backend and removes the /api prefix. So https://tailslip.com/api/v1/ideas reaches the backend route /v1/ideas. In local development the backend listens on http://localhost:8080 (no /api prefix).

All responses are JSON (Content-Type: application/json), except the SSE stream and a few browser-only auth redirects. Times are RFC 3339 in UTC. Prices and probabilities are fractions from 0 to 1. Money is in US dollars.

Authentication

Send one of these credentials. The server checks them in this order:

  1. Authorization: Bearer sm_... - an API key (recommended for scripts).
  2. Authorization: Bearer <JWT> - a session token.
  3. The session cookie - what the web app uses.

A request with no credential is anonymous. A request with a bad, expired or revoked credential is also treated as anonymous (not as an error). So if a protected endpoint returns 401 unauthorized, check your key.

Get an API key on the Account page, or with POST /v1/me/api-keys (see Account). The server shows the plain key once. It stores only a hash. Keys do not expire. Delete a key to revoke it.

export TS_KEY=sm_0123...   # your key
curl -H "Authorization: Bearer $TS_KEY" https://tailslip.com/api/v1/me

Plans and data delay

There are three kinds of callers. Pro comes from the user's plan in the database (or a server-side allowlist of emails).

CallerSmart-money dataPro-only endpoints
Anonymous1 hour delayed401 unauthorized
Free (signed in)1 hour delayed403 pro_required
ProLiveAllowed

1-hour delay (free and anonymous). These endpoints show data as it was 1 hour ago:

EndpointWhat is delayed
GET /v1/ideasThe consensus rows and the top_drivers trades
GET /v1/markets/{key}/consensuslatest and history_7d
GET /v1/markets/{key}/smart-holdersHoldings, rebuilt from snapshots as of 1 hour ago
GET /v1/markets/{key}/smart-tradesOnly trades older than 1 hour
GET /v1/events/{key}The consensus on each market
GET /v1/wallets/{key}/positionsPositions, rebuilt from snapshots as of 1 hour ago
GET /v1/wallets/{key}/tradesOnly trades older than 1 hour

These responses carry two extra top-level fields:

FieldMeaning
as_ofThe moment the data reflects. For pro this is now.
delay_seconds3600 for free and anonymous, 0 for pro.

A delayed consensus row must also be at most 2 hours older than the delay point (so between 1 and 3 hours old). If a market's signal stopped, it drops out of the delayed feed instead of showing its last value forever.

24-hour delay (paper book and system portfolios).

  • GET /v1/track-record: non-pro callers see every paper book as it stood 24 hours ago. Positions opened in the last 24 hours are hidden and counted in hidden_open_positions. Pro sees the current state.
  • GET /v1/portfolio/recommendations/{id} for a system (paper follower) recommendation: non-pro callers see only settled positions until the recommendation is 24 hours old or fully settled.

Not delayed for anyone: tags, market and event lists, a single market, wallet profiles, the top-wallets list, score history and search. These do not contain fresh smart-money signals.

Rate limits

Fixed 1-minute windows, kept in memory on the API server.

WhoLimitCounted per
Anonymous120 requests/minClient IP
Free240 requests/minUser
Pro600 requests/minUser
Everyone, before auth1200 requests/minClient IP
Sign-in endpoints (/v1/auth/email*, /v1/auth/magic-link)20 requests/minClient IP
POST /v1/portfolio/recommend30 requests/hourUser

Every rate-limited response (after auth) has these headers:

HeaderMeaning
X-RateLimit-LimitYour limit for the window
X-RateLimit-RemainingRequests left in the window
X-RateLimit-ResetUnix time when the window resets

Over the limit you get 429 with a Retry-After header (seconds) and details.retry_after_seconds in the body. Wait that long, then retry.

Response envelope

A successful read looks like this:

{
  "data": [ ... ],
  "next_cursor": "bzo1MA=="
}
  • data is an array or an object. Empty lists are [], never null.
  • next_cursor is null when there are no more pages.
  • Delayed endpoints add as_of and delay_seconds (see above).
  • Signal endpoints add disclaimer (a text string).

A few write endpoints return a small status object instead, for example {"status": "added"}. Each endpoint below shows its shape.

Errors

{
  "error": {
    "code": "pro_required",
    "message": "this feature requires a pro plan",
    "details": null
  }
}

Use code in your program. message is for people and can change. details is present only for some codes.

HTTPcodeWhen
400bad_requestMissing or bad parameter, or invalid JSON body
400bad_cursorafter on the fills feed is not a non-negative integer
400invalid_idA path id is not a number (recommendations, bet profiles)
400invalid_budget, invalid_risk, invalid_max_positions, invalid_min_price, invalid_horizon, invalid_tags, invalid_namePortfolio profile validation
401unauthorizedThe endpoint needs a signed-in user
401invalid_codeWrong or expired email sign-in code
403pro_requiredSigned in, but the endpoint needs pro
403session_requiredAPI-key management needs a browser session, not an API key
403bad_originA non-GET request from a browser on another site (see CSRF)
404not_foundMarket, event, wallet, recommendation or profile not found
404no_consensusThe market has no smart-money data (yet, or any more)
409track_staleRecommendation too old to track (over 24 h), or a market has no price
409track_resolvedRecommendation holds a closed or resolved market
413body_too_largeJSON body over 64 KiB (any body over 1 MiB is cut off)
415unsupported_media_typeBody is not application/json
429rate_limitedToo many requests (see above)
429too_many_streamsMore than 5 open SSE streams for one user
429email_rate_limitedToo many sign-in emails for one address
429too_many_attemptsToo many wrong sign-in codes
500internal, db_errorServer error
502books_unavailablePolymarket order books could not be read
502email_failedThe sign-in email could not be sent
503db_unavailable/health only: database down

An unknown path returns the router's plain-text 404 page not found, not the JSON envelope.

CSRF and request bodies

For POST and DELETE:

  • If the request has an Origin header, it must be the app's own origin. Scripts and bots normally send no Origin, so they pass.
  • A body must be Content-Type: application/json (only the magic-link form uses application/x-www-form-urlencoded).

Pagination

Most lists use an opaque cursor. Pass next_cursor back as ?cursor= to get the next page. Keep the other parameters the same. The cursor is an offset, so a list that changes between calls can skip or repeat rows.

ParameterMeaning
limitPage size. Values over the maximum are lowered to the maximum. Values of 0 or less are ignored.
cursorThe next_cursor from the last page.

next_cursor is null when the page had fewer rows than limit.

Exception: the trading-bot feed GET /v1/paper/follower/fills uses an id cursor (?after=<fill id>). See section 3.

Identifiers

  • Market {key}: the numeric TailSlip id, the market slug, or the Polymarket condition_id.
  • Event {key}: the numeric TailSlip id, the event slug, or the Polymarket event id.
  • Wallet {key}: the proxy wallet address (0x..., any case) or the numeric TailSlip id.

2. Endpoint reference

Common objects first, then the endpoints by group.

Common objects

Market

FieldMeaning
idTailSlip market id
event_idParent event id (may be absent)
condition_idPolymarket condition id
slug, questionPolymarket slug and question text
outcomesOutcome labels, for example ["Yes","No"]
token_idsPolymarket CLOB token id for each outcome, same order as outcomes
current_pricesLast price for each outcome, same order
active, closed, resolvedMarket status flags
resolved_outcomeIndex of the winning outcome, when resolved
end_dateScheduled end
liquidity_usd, volume_usdFrom Polymarket

Event: id, polymarket_id, slug, title, description, closed, neg_risk (a multi-outcome "negative risk" event), end_date, tags (list of Tag).

Tag: id, slug, label.

Consensus (what the tracked traders agree on in one market)

FieldMeaning
computed_atWhen this row was computed
outcome_indexThe outcome the smart money backs (absent if none)
strengthShare of smart stake on that outcome. This is agreement, not a probability.
smart_wallets_countSmart wallets with a position in the market (any side)
backers_countSmart wallets on the consensus side
weighted_holdings_usd, raw_holdings_usdSmart holdings, quality-weighted and raw
net_flow_24h_usd, net_flow_72h_usdNet smart buying (positive) or selling (negative)
current_market_pricePrice of the consensus outcome
implied_probThe model's probability for the outcome (shrunk and decayed)
q_impliedThe raw, unshrunk estimate (for transparency only)
shrink_betaHow much the raw estimate was pulled toward the price
divergenceimplied_prob - current_market_price. Positive means the model thinks the price is too low.
edge_remainingmax(0, divergence)
smart_entry_priceStake-weighted average entry of the smart money (null = unknown)
entry_gapCurrent price minus smart entry
edge_capturedShare of the smart money's entry edge already in the price
last_backing_atNewest buy on the consensus side
signal_age_secondscomputed_at - last_backing_at
exit_signaltrue when smart wallets are selling this outcome
exit_flow_24h_usdSize of that selling
idea_scoreRanking score for the ideas feed
confidencehigh, medium or low
modelshrink or calibrated
model_versionVersion of the calibrated model, if any
model_validatedtrue only for a calibrated model that beat the price on held-out data

Wallet: id, proxy_wallet, username, tier (1 = top, 2 = mid, 3 = probation; "smart" means tier 1 or 2), is_market_maker, excluded, bankroll_usd, first_seen_at, quality_score, roi_90d, resolved_count_90d. The last three come from the latest score and can be absent.

Smart trade (a trade by a tier 1 or 2 wallet): ts, outcome_index, side (0 = buy, 1 = sell), size_tokens, price, cash_usd, tx_hash, proxy_wallet, username, tier, quality_score.


Health

GET /health

No auth. Returns {"status":"ok","version":"0.1.0"}, or 503 db_unavailable. This response has no data wrapper.

curl https://tailslip.com/api/health

Ideas and markets

GET /v1/tags

No auth. All tags, sorted by slug. data: list of Tag.

curl https://tailslip.com/api/v1/tags

GET /v1/ideas

The main feed: markets where the smart-money consensus differs from the price. No auth. 1-hour delay for free and anonymous callers.

ParameterDefaultMeaning
min_divergence0.05Minimum divergence
min_smart_wallets3Minimum backers_count (wallets on the consensus side)
min_liquidity_usd10000Minimum market liquidity
min_price0Minimum price of the consensus outcome
max_price1Maximum price of the consensus outcome
tagnoneTag slug. Comma-separated list = any of these tags.
sortidea_scoreidea_score, divergence, recency (newest consensus) or promising (idea_score x price)
limit50Max 100
cursorFrom next_cursor

Only active markets are listed. Each data item:

FieldMeaning
marketMarket
event{id, slug, title} of the parent event (may be absent)
tagsTag slugs of the event
consensusConsensus
idea_scoreSame as consensus.idea_score
idea_score_percentileRank (0 to 1) among all active markets, before your filters
top_driversUp to 3 recent smart trades in this market (delayed like the feed)

Top level also has as_of, delay_seconds and disclaimer.

curl -H "Authorization: Bearer $TS_KEY" \
  "https://tailslip.com/api/v1/ideas?tag=politics,sports&min_divergence=0.08&limit=20"

GET /v1/markets

No auth. Sorted by volume, highest first. data: list of Market.

ParameterDefaultMeaning
statusopenopen (active), closed or all
activeLegacy: active=false with no status means all
tagnoneOne tag slug
limit50Max 100
cursorFrom next_cursor
curl "https://tailslip.com/api/v1/markets?tag=crypto&limit=10"

GET /v1/markets/{key}

No auth. data: one Market. 404 not_found if unknown.

curl https://tailslip.com/api/v1/markets/will-it-rain-tomorrow

GET /v1/markets/{key}/consensus

No auth. 1-hour delay for free and anonymous callers.

data:

FieldMeaning
latestThe newest Consensus row
history_7dConsensus rows of the last 7 days, oldest first

Returns 404 no_consensus when the market has no current consensus row. For a market that had a signal, this means the signal is gone.

curl -H "Authorization: Bearer $TS_KEY" \
  https://tailslip.com/api/v1/markets/12345/consensus

GET /v1/markets/{key}/smart-holders

No auth. 1-hour delay for free and anonymous callers. Smart wallets (tier 1 or 2, no market makers) that hold the market, largest first.

ParameterDefaultMeaning
limit20Max 100. There is no cursor; next_cursor is always null.

Each item: proxy_wallet, username, tier, quality_score, outcome_index, outcome (label), size_tokens, avg_price, cur_price, cash_pnl_usd, size_usd (current value of the position).

curl https://tailslip.com/api/v1/markets/12345/smart-holders?limit=10

GET /v1/markets/{key}/smart-trades

No auth. 1-hour delay for free and anonymous callers. Recent smart trades in the market, newest first.

ParameterDefaultMeaning
limit50Max 200. No cursor.

data: list of Smart trade.

curl https://tailslip.com/api/v1/markets/12345/smart-trades?limit=20

Events

GET /v1/events

No auth. Newest first. data: list of Event (without tags).

ParameterDefaultMeaning
statusopenopen (not closed), closed or all
tagnoneOne tag slug
limit50Max 100
cursorFrom next_cursor
curl "https://tailslip.com/api/v1/events?tag=politics"

GET /v1/events/{key}

No auth. 1-hour delay on the consensus for free and anonymous callers.

data:

FieldMeaning
eventEvent, with tags
marketsMarkets of the event (by volume). Each is a Market plus consensus (Consensus, absent when there is none).

Top level also has as_of, delay_seconds and disclaimer.

curl https://tailslip.com/api/v1/events/presidential-election-2028

Wallets

GET /v1/wallets/top

No auth. Scored wallets, not excluded ones. data: list of Wallet.

ParameterDefaultMeaning
sortqualityquality or roi. With roi, wallets with 5 or more resolved markets come first; wallets with no ROI come last.
limit50Max 100
cursorFrom next_cursor
curl "https://tailslip.com/api/v1/wallets/top?sort=roi&limit=25"

GET /v1/wallets/{key}

No auth. data: one Wallet. 404 not_found if unknown.

curl https://tailslip.com/api/v1/wallets/0xabc...def

GET /v1/wallets/{key}/positions

No auth. 1-hour delay for free and anonymous callers. Current positions, largest first. No paging.

Each item: market_id, market_slug, market_question, outcome_index, outcome, size_tokens, avg_price, cur_price, cash_pnl_usd, current_value_usd.

curl https://tailslip.com/api/v1/wallets/0xabc...def/positions

GET /v1/wallets/{key}/trades

No auth. 1-hour delay for free and anonymous callers. Newest first.

ParameterDefaultMeaning
limit50Max 200
cursorFrom next_cursor

Each item: ts, outcome_index, side (0 = buy, 1 = sell), size_tokens, price, cash_usd, tx_hash, market_slug, market_question.

curl "https://tailslip.com/api/v1/wallets/0xabc...def/trades?limit=100"

GET /v1/wallets/{key}/score-history

No auth. The last 90 score rows (not 90 days), newest first.

Main fields of each row: computed_at, quality_score, pnl_30d_usd, pnl_90d_usd, roi_30d, roi_90d, volume_90d_usd, deployed_90d_usd, trade_count_90d, distinct_markets_90d, win_rate_resolved, resolved_count_90d, sharpe_like, consistency_score, skill_score, skill_n_eff, brier, brier_price (lower is better; brier < brier_price means copying the wallet beat the price), scoring_status (scored, probation, thin, market_maker, arbitrage or quarantined), unmatched_sell_ratio, recon_mismatch, arb_ratio.

curl https://tailslip.com/api/v1/wallets/0xabc...def/score-history

GET /v1/search?q=<text>

No auth. Text match on event titles and slugs, market questions and slugs, and wallet usernames (wallet addresses match by prefix). Up to 10 of each. q is required (400 bad_request if empty).

data: { "events": [Event], "markets": [Market], "wallets": [Wallet] }. Search wallets have no score fields.

curl "https://tailslip.com/api/v1/search?q=bitcoin"

Track record and paper book

The paper follower is a simulated book that runs every hour (by default) with fake money. It buys what the portfolio model picks and sells when the signal decays. It pays modeled fees and slippage. Two benchmark books take the same stake sizes: favorite and placebo. The follower only shows an edge if it beats the placebo over time.

GET /v1/track-record

No auth. Non-pro callers see every book as of 24 hours ago. Pro sees the current state.

ParameterDefaultMeaning
limit30Max 100. Pages the legacy entries list.
cursorFrom next_cursor

data.summary:

FieldMeaning
bookThe follower book (PaperBookSummary, null before its first run)
benchmarksThe favorite and placebo books
disclosureText that states the latency, cost and marking assumptions
recommendations, positions, settled, voided, open, deployed_settled_usd, realized_pnl_usd, roi, hit_rate, avg_q_settledLegacy totals from the old nightly system portfolios
legacy_noteWhy the legacy totals are not the track record

A PaperBookSummary has:

FieldMeaning
name, descriptionfollower, favorite or placebo
inception_at, last_run_atStart and last run
starting_cash_usd, cash_usdCash at start and now
positions_value_usdOpen positions at their bid-side marks
equity_usdCash plus positions
roiequity / starting cash - 1 (realized and unrealized)
realized_pnl_usd, unrealized_pnl_usd, fees_usd
max_drawdownLargest fall from a peak, as a share of the peak
open_positions, closed_positions, settled_positionsClosed = sold before resolution. Settled = held to resolution.
hit_rateShare of closed and settled positions with profit
equity_curveDaily points: ts, cash_usd, positions_value_usd, equity_usd, open_positions
positionsFollower only: all open positions and the 30 newest closed or settled
hidden_open_positionsOpen positions hidden by the 24-hour delay
paramsThe book's settings (interval, fees, slippage and more)

Each paper position has market_id, outcome_index, question, market_slug, event_slug, outcome_label, status (open, closed, settled), shares, cost_usd, entry_price, entry_q, signal_price, signal_at, opened_at, closed_at, exit_reason, exit_price, mark_price, value_usd, realized_pnl_usd, unrealized_pnl_usd, fees_usd, provisional (settled on a resolution that is not final yet).

data.entries is the legacy list of system recommendations (headers only), newest first. Each has the Recommendation header fields (below) plus locked and delayed_until.

Top level: data, next_cursor, disclaimer.

curl https://tailslip.com/api/v1/track-record

GET /v1/paper/follower/fills

Pro only. The follower book's trade ledger, oldest first. This is the feed for trading bots. See section 3 for the full description.

ParameterDefaultMeaning
after0Return fills with id greater than this
limit1001 to 500. A value outside this range is replaced by 100.
curl -H "Authorization: Bearer $TS_KEY" \
  "https://tailslip.com/api/v1/paper/follower/fills?after=0&limit=500"

Portfolio

The portfolio engine turns the ideas feed into stake sizes for a budget. It reads live Polymarket order books. The model is unvalidated.

POST /v1/portfolio/recommend

Pro only. Limit: 30 valid requests per hour per user. The call can take up to a few minutes (it reads up to 200 order books). Every call is saved as a recommendation.

Body (JSON). Send either profile_id (a saved bet profile; then the other fields are ignored) or the fields inline:

FieldDefaultRule
profile_idId of one of your bet profiles
budget_usd(required)50 to 50000
riskbalancedconservative, balanced or aggressive
tagsanyUp to 20 tag slugs, 1 to 64 characters each
horizon_days00 (any) to 365. Only markets that end within this many days.
max_positions83 to 20
min_price0.150 to 0.5. Send 0 to remove the floor.

data:

FieldMeaning
idRecommendation id
algo_versionEngine version, for example portfolio-v2
consensus_batchTime of the consensus data used
profileThe profile that was used, after defaults
positionsThe sized positions (below)
statsTotals (below)

Each position: market_id, outcome_index, question, market_slug, event_slug, outcome_label, price_at_rec (live mid), fill_price (average price from walking the order book; the PnL basis), implied_prob, stake_usd, shares (whole shares), kelly_raw (Kelly fraction before caps), binding_cap (the rule that set the final size), rationale (the top smart trades), fee_rate_bps, fee_usd, effective_price (fill + fee per share + haircut), edge_at_fill (implied_prob - effective_price), limit_price (worst book level touched, on the tick grid), cluster.

stats: deployed_usd, fees_usd, e_profit_usd (expected profit under the model), p_loss (chance of a loss under the model, Monte Carlo), p_loss_basis, max_loss_usd, median_end, n_candidates, n_dropped (count of refused candidates by reason, for example no_edge, edge_after_slippage, no_book_depth, below_min_order, dust, budget, tag_cap, cluster_cap, max_positions, confidence, horizon, price_band, liquidity, no_token, no_book, pre_edge, event_correlated, exit_signal, stale_signal, model_unvalidated), model, model_validated.

An empty positions list is a valid answer. n_dropped tells you why.

curl -X POST -H "Authorization: Bearer $TS_KEY" -H "Content-Type: application/json" \
  -d '{"budget_usd": 500, "risk": "balanced", "max_positions": 6}' \
  https://tailslip.com/api/v1/portfolio/recommend

GET /v1/portfolio/recommendations

Signed in (any plan). Your own recommendations, newest first.

ParameterDefaultMeaning
limit20Max 100
cursorFrom next_cursor

Each item (a Recommendation header): id, system, origin (system or user), legacy, profile, algo_version, consensus_batch, tracked, created_at, stats, positions_count, settled_count, voided_count, deployed_usd, realized_pnl_usd.

curl -H "Authorization: Bearer $TS_KEY" https://tailslip.com/api/v1/portfolio/recommendations

GET /v1/portfolio/recommendations/{id}

No auth gate. You can read your own recommendations and any system (paper follower) recommendation. Other users' recommendations return 404 not_found.

data:

FieldMeaning
recommendationRecommendation header
positionsPositions: market_id, outcome_index, question, market_slug, event_slug, outcome_label, price_at_rec, fill_price, implied_prob, stake_usd, shares, kelly_raw, binding_cap, rationale, current_price, resolved, resolved_won, exit_price, pnl_usd
lockedtrue when unsettled picks are hidden from you
delayed_untilWhen the lock ends
hidden_positionsNumber of hidden positions

A system recommendation is locked for non-pro callers until it is 24 hours old or fully settled.

curl https://tailslip.com/api/v1/portfolio/recommendations/42

POST /v1/portfolio/recommendations/{id}/track

Signed in. Marks your recommendation as "tracked" (you say you placed it). No body. If more than 15 minutes have passed, the positions are re-priced at the current market price. Refused with 409 track_stale after 24 hours, and with 409 track_resolved if a market has closed. Calling it again is safe.

Returns {"data": {"id": 42, "tracked": true}}.

curl -X POST -H "Authorization: Bearer $TS_KEY" \
  https://tailslip.com/api/v1/portfolio/recommendations/42/track

Bet profiles

Saved inputs for POST /v1/portfolio/recommend. Signed in (any plan).

Method and pathBodyReturns
GET /v1/me/bet-profilesdata: list of {id, name, budget_usd, risk, tags, horizon_days, max_positions, min_price, created_at}
POST /v1/me/bet-profilesSame fields as recommend, plus name (max 64 characters, default default). Saving an existing name replaces it.{"data": {"id", "name"}}
DELETE /v1/me/bet-profiles/{id}{"data": {"deleted": true}}
curl -X POST -H "Authorization: Bearer $TS_KEY" -H "Content-Type: application/json" \
  -d '{"name": "small", "budget_usd": 200, "risk": "conservative"}' \
  https://tailslip.com/api/v1/me/bet-profiles

Streaming

GET /v1/stream/consensus

Pro only. A Server-Sent Events stream. The server sends an event each time it writes a fresh consensus for a market, including when a market's signal disappears. At most 5 open streams per user (429 too_many_streams).

retry: 5000

event: consensus
data: {"market_id":12345}

event: heartbeat
data: {"ts":"2026-11-08T12:00:15Z"}

event: unauthorized
data: {}
  • consensus: the market changed. Read GET /v1/markets/{id}/consensus for the new values. A 404 no_consensus there means the signal is gone.
  • heartbeat: every 15 seconds. If you see none for a minute, reconnect.
  • unauthorized: sent once, then the stream closes. The server re-checks your credential about every 60 seconds. You see this if your key was deleted, your session ended or your plan is no longer pro. Do not reconnect with the same credential.

If your client is slow, the server can drop events for you. Treat the stream as a hint to re-read, not as a complete log.

curl -N -H "Authorization: Bearer $TS_KEY" https://tailslip.com/api/v1/stream/consensus

Account

All account endpoints need a signed-in user (any plan). An API key counts as signed in.

GET /v1/me

data: {id, email, plan, created_at}. plan is free or pro.

curl -H "Authorization: Bearer $TS_KEY" https://tailslip.com/api/v1/me

API keys

Method and pathBodyReturns
GET /v1/me/api-keysdata: list of {id, label, last_used_at, created_at}
POST /v1/me/api-keysOptional {"label": "..."} (max 100 characters)201 with {"data": {id, label, created_at}, "key": "sm_..."}. Save key now; it is shown only once.
DELETE /v1/me/api-keys/{id}{"status": "deleted"}

Creating and deleting keys needs a signed-in browser session. A request authenticated with an API key gets 403 session_required, so a leaked key cannot create more keys or revoke yours. Manage keys on the Account page.

Watchlist

Method and pathBodyReturns
GET /v1/me/watchlistdata: list of Market, newest added first
POST /v1/me/watchlist{"market_id": 12345} (numeric id)201 with {"status": "added"}
DELETE /v1/me/watchlist/{marketID}{"status": "removed"}
curl -X POST -H "Authorization: Bearer $TS_KEY" -H "Content-Type: application/json" \
  -d '{"market_id": 12345}' https://tailslip.com/api/v1/me/watchlist

Auth (web app)

These endpoints exist for the web app's sign-in page. Scripts should use an API key instead. Sign-in endpoints have a limit of 20 requests per minute per IP. A session lasts 30 days.

Method and pathPurpose
GET /v1/auth/googleStarts Google sign-in (redirects to Google)
GET /v1/auth/google/callbackGoogle returns here; sets the session cookie and redirects to the app
POST /v1/auth/emailBody {"email"}. Sends a 6-digit code and a link, valid 15 minutes. Returns {"status": "sent"}.
POST /v1/auth/email/verifyBody {"email", "code"}. Sets the session. Returns {"data": user}.
GET /v1/auth/magic-link?token=Shows a confirm page (so mail scanners do not use up the link)
POST /v1/auth/magic-linkForm field token. Sets the session and redirects to the app.
POST /v1/auth/logoutClears the cookie and ends all browser sessions of the user. API keys stay valid. Sent with an API key, it ends no sessions. Returns {"status": "ok"}.

A client without a cookie store can send X-Want-Session-Token: 1 on a sign-in request. The server then returns the JWT in the X-Session-Token response header.


3. Building a trading bot

Read this first. The strategy is not validated. The paper follower has a short history. Treat it as unproven until it beats its placebo benchmark on GET /v1/track-record over a long enough period, after fees and slippage. Until then, a bot that copies it is an experiment. Use money you can lose completely. Nothing here is investment advice, and Polymarket is not available in some places, including the US.

The idea

The paper follower book trades with fake money once an hour (by default). It records every trade in a ledger. GET /v1/paper/follower/fills gives you that ledger. Your bot reads new rows and places the same trades on Polymarket at a smaller or larger size. Then your live results repeat what the paper test measured, as closely as real fills permit.

TailSlip never holds your wallet keys and never places orders. Your bot runs on your machine. It signs orders with your own key, through Polymarket's CLOB client (for example py-clob-client). The TailSlip API key only reads data.

The fills feed

GET /v1/paper/follower/fills?after=<id>&limit=<n> (pro only)

ParameterDefaultMeaning
after0Return fills with id greater than this. Must be a non-negative integer (400 bad_cursor otherwise).
limit1001 to 500. A value outside this range is replaced by 100.

The rows come oldest first (ascending id). Each row:

FieldMeaning
idFill id. Use it as your cursor. Ids are shared with other paper books, so gaps are normal.
tsTime of the paper fill
kindbuy, sell, settle or correction
reasonWhy (table below)
position_idThe paper position. Several fills can share it (entry, top-ups, exit).
market_id, market_slug, questionThe market
condition_idPolymarket condition id
outcome_index, outcome_labelThe outcome traded
token_idThe Polymarket CLOB token id to trade
sharesShares in this fill (whole shares for buy and sell)
priceSimulated average fill per share, not an order price (can be off the tick grid). buy: average ask from the book walk plus the simulated slippage. sell: average bid minus slippage. settle and correction: the payout per share (1 = won, 0 = lost, 0.5 = 50/50).
fee_usdModeled taker fee (separate from price)
cash_delta_usdChange to the paper book's cash (negative for a buy)
implied_probThe model's probability at entry (buy rows only)
position_shares_nowThe position's shares now, not just after this fill
position_status_nowThe position's status now: open, closed or settled

Kinds and reasons:

kindreasonWhat happenedWhat your bot does
buyentryA new positionBuy
buytopupMore of a held positionBuy
sellsignal_goneThe smart-money signal for the market went awaySell
sellconsensus_flipThe consensus moved to another outcomeSell
sellfew_backersToo few smart wallets still back the outcomeSell
sellexit_signalSmart wallets are selling the outcomeSell
sellbacker_outflowLarge net selling by backers in 24 hoursSell
selltake_profitThe bid reached the model's probabilitySell
settleresolvedThe market resolved; the paper position was redeemedNo order. Redeem your winning tokens.
correctionpayout revisedA provisional resolution was correctedNo order. Bookkeeping only.

A sell can be partial when the order book was thin. Then position_status_now stays open and a later sell can follow.

What the feed does not give you.

  • price is the simulated average fill of the paper book: the book-walk average plus slippage for a buy, minus slippage for a sell. It is not an order price. It can be off the tick grid.
  • The row has no tick size, no minimum order size, no neg-risk flag and no limit price. Before each order, read the fresh CLOB order book and the market metadata for token_id (tick size, minimum order size, neg_risk) from Polymarket. Then set your own limit price on the tick grid.
  • position_shares_now and position_status_now show the position now. They do not tell you how much was held before a given fill. To get the share of a position that a sell closed, replay the ledger by position_id: add the shares of each buy row and subtract the shares of each sell row, in id order. The fraction sold is sell.shares / paper shares held just before the sell.
  • settle and correction rows need no CLOB order.

Paging. When a page is full (limit rows), next_cursor is the last row's id as a string. Call again with after=<next_cursor>. When the page is short, next_cursor is null and you are up to date. Note that this cursor is a plain id, not the opaque cursor of the other lists.

{
  "data": [
    {
      "id": 9812,
      "ts": "2026-11-08T11:00:42Z",
      "kind": "buy",
      "reason": "entry",
      "position_id": 377,
      "market_id": 12345,
      "market_slug": "example-market",
      "question": "Will X happen by December 31?",
      "condition_id": "0x5f...",
      "outcome_index": 0,
      "outcome_label": "Yes",
      "token_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
      "shares": 412,
      "price": 0.347,
      "fee_usd": 2.33,
      "cash_delta_usd": -145.3,
      "implied_prob": 0.41,
      "position_shares_now": 412,
      "position_status_now": "open"
    }
  ],
  "next_cursor": null
}

(Example values are made up. Field names are exact.)

The polling loop

  1. Store the last id you handled, on disk. On the first run, read the whole ledger from after=0 without trading. Build the paper share count for each position_id, and save the last id. Then trade only new rows. Positions that were already open when you started are not yours; ignore their later sells (your own share count for them is 0).
  2. Poll every few minutes, for example every 5 minutes. The paper book runs about once an hour, so faster polling gains little. 12 calls per hour is far below the pro limit of 600 per minute.
  3. Read all pages: repeat with after=<next_cursor> while it is not null.
  4. Handle each row once, in id order. Save the id after you handle the row. Then a crash does not repeat or skip a trade.

Mirror at a scale factor

Choose a scale factor, for example 0.05 (5% of the paper size). The paper book starts with its starting_cash_usd (see /v1/track-record).

Keep two numbers for each position_id: the paper shares (from the ledger replay) and your own shares (from your real fills).

  • Buy: size = floor(shares x scale). Read the fresh order book for token_id. Use a limit order, not a market order. Use the row's price only as a guide: it is a simulated average that already includes slippage. Round your limit to the market's tick size. Skip the trade if the best ask is now above price plus your tolerance. Do not chase.
  • Sell: compute the fraction sold from the replay (sell.shares / paper shares before the sell). Sell that fraction of your own shares for the position (all of them when the paper position is now fully sold). Read the fresh bid side and set a limit on the tick grid near the row's price.
  • Too small: Polymarket has a minimum order size per market and a tick size. Read both from the market metadata; do not assume them. Skip orders under the minimum. Your own share count for the position then stays lower, so later sells scale correctly.
  • Neg-risk markets need the neg-risk flag when you sign the order. Read it from Polymarket's market metadata for the token.
  • Settle and correction: place no order. When a market resolves, redeem your winning tokens (in the Polymarket app or with an on-chain redeem call). py-clob-client does not redeem.

Your fills will differ from the paper fills. You trade minutes after the paper book, and the book can move. Track your own results and compare them with the paper book.

Risk controls (build these in)

  • Kill switch: a file or an environment flag that stops all new orders. Check it before every order.
  • Hard limits: maximum dollars per order, per market and in total. A maximum daily loss that stops the bot.
  • Price guard: refuse prices outside 0.01 to 0.99, and refuse any limit far from the row's price.
  • Stale guard: skip a buy row older than, for example, 2 hours (ts).
  • Dry run first: log the orders without sending them for a few days.
  • Alerts: send yourself a message on each trade and on every error.
  • Keys: keep the Polymarket private key out of source code. Use a wallet that holds only the money for this bot.

Optional: earlier exits with the stream

The paper book checks exits only once an hour. If you want to react sooner, open GET /v1/stream/consensus (pro) as a wake-up signal. On a consensus event for a market you hold, read GET /v1/markets/{market_id}/consensus:

  • latest.exit_signal is true: smart wallets are selling.
  • latest.outcome_index is not your outcome: the consensus flipped.
  • 404 no_consensus: the signal is gone.

An early exit is your own rule. The paper test did not measure it, so your results can differ from the track record. You can also simply poll the fills feed when an event arrives, instead of on a timer.

Python sketch

This is a sketch, not a working bot. It shows the structure only. The py-clob-client calls are written from memory of its public API. Check them against the library's current documentation. Test with dry runs and tiny sizes first.

# SKETCH ONLY - not tested. Check every py-clob-client call against its docs.
import json, math, os, time, pathlib
import requests

API = "https://tailslip.com/api"
TS_KEY = os.environ["TAILSLIP_API_KEY"]          # sm_... (read-only data key)
STATE = pathlib.Path("bot_state.json")            # {"last_id": 0, "paper": {pos_id: shares}, "mine": {pos_id: shares}}
KILL = pathlib.Path("STOP")                       # create this file to stop trading
SCALE = 0.05                                      # 5% of the paper size
MAX_ORDER_USD = 50
PRICE_TOLERANCE = 0.02
DRY_RUN = True

def load_state():
    return json.loads(STATE.read_text()) if STATE.exists() else {"last_id": 0, "paper": {}, "mine": {}}

def save_state(s):
    STATE.write_text(json.dumps(s))

def fetch_new_fills(after):
    """Yield fills with id > after, oldest first, across all pages."""
    while True:
        r = requests.get(f"{API}/v1/paper/follower/fills",
                         params={"after": after, "limit": 500},
                         headers={"Authorization": f"Bearer {TS_KEY}"}, timeout=30)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "60")))
            continue
        r.raise_for_status()
        body = r.json()
        yield from body["data"]
        if body["next_cursor"] is None:
            return
        after = int(body["next_cursor"])

# --- Polymarket client (your key, your wallet; TailSlip never sees it) ---
# from py_clob_client.client import ClobClient
# from py_clob_client.clob_types import OrderArgs, OrderType
# from py_clob_client.order_builder.constants import BUY, SELL
# clob = ClobClient("https://clob.polymarket.com", key=os.environ["PK"], chain_id=137,
#                   signature_type=..., funder=os.environ.get("FUNDER"))
# clob.set_api_creds(clob.create_or_derive_api_creds())

def market_meta(token_id):
    """Fresh tick size, min order size, neg-risk flag and best bid/ask for the token.
    The fills feed carries none of these. Read them from Polymarket, e.g.:
    # book = clob.get_order_book(token_id)   # bids/asks, tick_size, min_order_size, neg_risk (check fields)
    """
    raise NotImplementedError

def to_tick(price, tick, side):
    # BUY rounds down, SELL rounds up, so the limit is never worse than intended.
    n = price / tick
    return round((math.floor(n) if side == "BUY" else math.ceil(n)) * tick, 6)

def place_limit(token_id, side, size, guide_price):
    if KILL.exists():
        raise SystemExit("kill switch on")
    m = market_meta(token_id)
    best = m["best_ask"] if side == "BUY" else m["best_bid"]
    if side == "BUY" and best > guide_price + PRICE_TOLERANCE:
        print("skip: ask moved", token_id, best, guide_price)
        return 0
    if side == "SELL" and best < guide_price - PRICE_TOLERANCE:
        print("skip: bid moved", token_id, best, guide_price)
        return 0
    price = to_tick(guide_price, m["tick_size"], side)   # fill price is an average, off-grid
    if size < m["min_order_size"] or size * price > MAX_ORDER_USD or not 0.01 <= price <= 0.99:
        print("skip", side, token_id, size, price)
        return 0
    print(("DRY " if DRY_RUN else "") + f"{side} {size} @ {price} token={token_id}")
    if DRY_RUN:
        return size
    # order = clob.create_order(OrderArgs(token_id=token_id, price=price, size=size, side=side),
    #                           options=PartialCreateOrderOptions(tick_size=..., neg_risk=m["neg_risk"]))
    # resp = clob.post_order(order, OrderType.GTC)   # or FOK/FAK; check fills afterwards
    # return <shares actually filled, from resp / clob.get_order(...)>
    return 0

def handle(fill, state):
    pid = str(fill["position_id"])
    tok = fill["token_id"]
    paper = state["paper"].get(pid, 0)            # paper shares before this fill (ledger replay)
    mine = state["mine"].get(pid, 0)              # my real shares for this position
    if fill["kind"] == "buy":
        size = math.floor(fill["shares"] * SCALE)
        got = place_limit(tok, "BUY", size, fill["price"])
        state["paper"][pid] = paper + fill["shares"]
        state["mine"][pid] = mine + got
    elif fill["kind"] == "sell":
        frac = 1.0 if paper <= 0 else min(1.0, fill["shares"] / paper)
        if fill["shares"] >= paper - 1e-9:
            frac = 1.0                            # paper position fully closed: sell everything
        size = math.floor(mine * frac)
        got = place_limit(tok, "SELL", size, fill["price"]) if size > 0 else 0
        state["paper"][pid] = max(0, paper - fill["shares"])
        state["mine"][pid] = mine - got
    else:
        # settle / correction: no CLOB order. Redeem winning tokens separately.
        print("no order:", fill["kind"], fill["reason"], fill["market_slug"])

def bootstrap(state):
    """First run: replay the paper ledger without trading (paper shares only)."""
    for fill in fetch_new_fills(0):
        pid = str(fill["position_id"])
        if fill["kind"] == "buy":
            state["paper"][pid] = state["paper"].get(pid, 0) + fill["shares"]
        elif fill["kind"] == "sell":
            state["paper"][pid] = max(0, state["paper"].get(pid, 0) - fill["shares"])
        state["last_id"] = fill["id"]
    save_state(state)

def main():
    state = load_state()
    if not STATE.exists():
        bootstrap(state)
    while True:
        try:
            for fill in fetch_new_fills(state["last_id"]):
                handle(fill, state)
                state["last_id"] = fill["id"]
                save_state(state)                 # save after each row: no repeats after a crash
        except requests.RequestException as e:
            print("poll failed:", e)              # alert yourself here
        time.sleep(300)                           # the paper book runs about hourly

if __name__ == "__main__":
    main()

Checklist before real money

  • The follower beats placebo on /v1/track-record over a long enough period, after fees.
  • Your dry run ran for days without errors.
  • Kill switch, order limits and daily loss limit work.
  • Your bot keeps its own record of positions and compares it with Polymarket.
  • You are allowed to trade on Polymarket where you live.