Screener
Rank every indexed wallet on expected value per dollar rather than win rate, filter on any column, and call the same screen from the API.
The screener ranks every indexed wallet on a statistic that prices in the odds
each wallet paid. By the end of this page you will know which column to sort on,
which columns are deliberately unsortable, how to shape a screen with bounds and
filters, and how to fetch the same screen from GET /api/v1/screener.
How the ranking works
Six columns carry the V5 skill statistics. They are computed per wallet from resolved outcomes and refreshed by the stats pipeline.
| Column | Param | What it answers |
|---|---|---|
| EV lower confidence bound | ev_ci_low | How much edge survives the sample. The default ranking column for skill. |
| EV per dollar | ev_per_dollar | Point estimate of edge per dollar staked. |
| EV per dollar (decayed) | ev_decayed | Same, with recent trades weighted more heavily. |
| Skill q-value (FDR) | skill_qvalue | False-discovery-rate q-value. Lower is stronger. Sort ascending. |
| Effective sample size | effective_n | The sample after correlated trades on one event are collapsed. |
| Skill z (recency-decayed) | z_decayed | Decayed z-score, clustered by event. |
Two properties are worth stating explicitly because they change how you read a board:
- z-scores are clustered by event. Twenty positions on one election are one
observation, not twenty. That is what
effective_nreports, and it is why a wallet with 400 trades can carry an effective n in the dozens. - q-values, not p-values. Scoring three million wallets produces thousands of p < 0.01 wallets by chance alone. The q-value is the false-discovery rate at that threshold, so a q-value of 0.05 means roughly one in twenty wallets at or below it is noise.
Win rate filters, but never sorts
Win rate ignores the price paid, and on a prediction market the price is the whole game. One wallet in the index settles 6,364 positions at an average entry of 99.6¢: it wins 99.86% of them, earns 0.25¢ per dollar staked, and finishes negative after fees. Another wins 44.3% and makes money, because it buys longshots that resolve. Ranking on win rate sorts the first above the second — the numbers are right and the ordering is a lie.
So bayesian_win_rate and confidence stay visible as columns and stay usable
as filters, and the API refuses them as a sort key:
{
"error": {
"code": "bad_request",
"message": "bayesian_win_rate cannot be a sort key. Ignores entry price — 99% win rates are usually 99c entries. Rank by EV lower bound instead.",
"request_id": "req_..."
}
}A bound is a claim about one wallet; a sort is a claim that one wallet beats another. The first is fine on win rate, the second is not.
Columns
Every column below can be requested with columns=, bounded with
min_<column> / max_<column>, and — unless noted — used as sort=.
| Param | Label | Sortable |
|---|---|---|
pnl | Profit (canonical) | yes |
polymarket_pnl | Profit (Polymarket-reported) | yes |
ledger_pnl | Profit (our ledger) | yes |
total_volume | Volume | yes |
avg_bet_size | Average bet size | yes |
bayesian_win_rate | Win rate (shrunk) | filter only |
confidence | Win-rate confidence | filter only |
resolved_bets | Resolved bets | yes |
total_bets | Total bets | yes |
markets_traded | Markets traded | yes |
z_decayed | Skill z (recency-decayed) | yes |
ev_per_dollar | EV per dollar | yes |
ev_ci_low | EV lower confidence bound | yes |
ev_decayed | EV per dollar (decayed) | yes |
effective_n | Effective sample size | yes |
skill_qvalue | Skill q-value (FDR) | yes |
markout_1h | Markout 1h | yes, but unpopulated |
markout_24h | Markout 24h | yes, but unpopulated |
last_active_at | Last fill | yes |
last_updated | Stats refreshed | yes |
pnl is the canonical profit column: Polymarket's reported figure when one
exists, our own ledger otherwise. polymarket_pnl and ledger_pnl expose the
two inputs separately so you can compare them yourself.
The default sort is pnl descending, and the default columns are pnl,
total_volume, bayesian_win_rate, confidence, resolved_bets,
markets_traded, last_active_at.
Columns with no writer yet
markout_1h and markout_24h are in the catalog but no pipeline populates them
yet. Sorting or filtering on one returns an empty page, which is a missing
pipeline rather than a missing population — so the response names them:
"empty_columns": ["markout_1h"]Filters
trader_kind—any,human,bot,makerorunknown. Every wallet carries one verdict: a bot is at least 10,000 lifetime fills across 250 or more markets; a maker earns maker rebates at least five times its taker rebates at machine scale; anything with fills that trips neither rule is human. Filtering tohumanis the fastest way to drop market makers off a board.active_within—24h,7dor30d, applied to the wallet's most recent fill. A wallet with no recorded fill fails the filter.min_<column>/max_<column>— numeric bounds on any numeric column, in the column's own units. Combine freely:min_resolved_bets=100andmax_skill_qvalue=0.05is "at least a hundred resolved bets, and significant after false-discovery correction".
Screens are one string
A screen — sort, direction, filters, bounds, columns, window — encodes into a
single state token, versioned with an s1. prefix. The UI and the API share the
codec, so a screen you shape in the browser and a screen you call from a script
are the same object, and a link you paste to someone reproduces exactly what you
were looking at.
Pass it as state=, then override any single field with an ordinary parameter:
GET /api/v1/screener?state=<token>&sort=ev_ci_low&dir=descEvery response echoes the normalized token back as state, so you can capture
the screen you actually got rather than the one you thought you asked for.
GET /api/v1/screener
Minimum tier free. Results are cursor-paginated with a keyset cursor, so a page
boundary survives the stats refresher rewriting rows underneath you.
curl -G "https://crowdintel.xyz/api/v1/screener" \
-H "Authorization: Bearer $CROWDINTEL_KEY" \
--data-urlencode "sort=ev_ci_low" \
--data-urlencode "dir=desc" \
--data-urlencode "trader_kind=human" \
--data-urlencode "min_resolved_bets=100" \
--data-urlencode "max_skill_qvalue=0.05" \
--data-urlencode "columns=pnl,ev_ci_low,skill_qvalue,effective_n,resolved_bets" \
--data-urlencode "limit=25"The envelope carries the screen's metadata as siblings of data, and every row
keys its metrics by the same param name you sorted and filtered on:
{
"state": "s1.eyJzIjoiZXZfY2lfbG93Iiwi...",
"sort": "ev_ci_low",
"dir": "desc",
"columns": ["pnl", "ev_ci_low", "skill_qvalue", "effective_n", "resolved_bets"],
"empty_columns": [],
"depth_cap": 25,
"tier": "free",
"data": [
{
"address": "0x...",
"trader_kind": "human",
"trader_kind_label": "Human",
"polymarket_name": "...",
"polymarket_avatar": "https://...",
"pnl": 412803.11,
"ev_ci_low": 0.031,
"skill_qvalue": 0.004,
"effective_n": 61.4,
"resolved_bets": 388
}
],
"next_cursor": "eyJ2Ijo..."
}Parameters
| Param | Values | Default |
|---|---|---|
state | encoded screen token | none |
sort | any sortable column param | pnl |
dir | asc, desc | desc |
trader_kind | any, human, bot, maker, unknown | any |
active_within | 24h, 7d, 30d | none |
min_<column> / max_<column> | number | none |
columns | csv of column params | the seven default columns |
window | all | all |
limit | 1–100 | 25 |
cursor | opaque cursor from next_cursor | none |
window accepts only all today. Windowed PnL columns are not shipped, and the
endpoint returns 400 rather than answering a 7-day question with lifetime
numbers.
How deep you can page
depth_cap is how far into the ranking a tier may page, and the cursor carries
the depth so the cap survives paging.
| Tier | Depth cap |
|---|---|
| Free | 25 |
| Terminal | 500 |
| Terminal Pro | 1,000 |
Every tier sees the same columns and the same ranking; the cap is how far down
the board you may walk. At the cap, next_cursor comes back null — a free key
gets one page of up to 25 rows and no continuation. Replaying a cursor against a
different screen is a 400 rather than a silently different page, so keep the
query identical while paging.
Shape the screen with bounds rather than paging to find what you want:
min_resolved_bets=100 with max_skill_qvalue=0.05 puts what you were looking
for on page one.
Next
- Cohorts and lists — turn a screen into a set of wallets you actually watch.
- Copy trade safely — the method for going from a screener row to a position.
