Polyrankdocs
Product guides

Ranking Builder

Build your own composite ranking from the metric catalog.

The Builder (polyrank.app/rankings/builder, paid plans) lets you weight any combination of catalog metrics into a custom composite and rank the full wallet universe against it.

The Ranking Builder: weight sliders on selected metrics, universe filters, and the live top-20 preview recomputing as you drag

Drag a weight slider and the live preview re-ranks the full wallet universe (3M+ wallets) — the top-20 column updates as you compose.

Via API

curl -s -X POST https://api.polyrank.app/v1/rankings/custom \
  -H "Authorization: Bearer pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "weights": {
      "brier":        { "weight": 40, "window": "lifetime" },
      "alpha_vs_mid": { "weight": 30, "window": "30d" },
      "sharpe":       { "weight": 30, "window": "lifetime" }
    },
    "filters": { "min_trades": 100, "min_volume_usdc": 10000 },
    "limit": 50
  }'

Every weight is { "weight", "window" } (window defaults to lifetime) on any of the 84 catalog metrics (GET /v1/metrics/catalog), at any window that metric exists at — GET https://api-agent.polyrank.app/v1/agent/metrics/windows lists which metric exists at which window (free). A metric asked for at a window it has no data in is a 400 that names the windows it does have. The copy-engine score (sk_score_14d) and the entry-timing ids are weightable at lifetime (their window is part of the id).

  • Weights apply to percentile-normalized metric values; direction (higher/lower-is-better) is handled per the catalog.
  • Filters cut the universe before ranking — sample-size floors keep small-N noise out. Beyond the fixed gates, metric_filters puts an absolute min/max on ANY displayable metric's raw lifetime value ({"metric_filters": [{"metric": "roi_pct", "min": 0, "window": "30d"}]}, max 12; wallets where the metric isn't measured are excluded), and skill_gates is the relative version (top-X% by percentile, max 8). Both take a window. The copy filters — copy_engine (the engine's validated sleeve), timing (an entry-timing slice), cluster_policy (one wallet per wallet farm), copy_placeable, active_within_hours, smart_money_tags_any_of, follower_count_max, categories, exclude_bonders — are all applied; market_kinds / exchange_versions are refused (no per-wallet data exists for them).
  • Sorting: sort_by accepts composite (default), a raw stat (realized_pnl, roi_pct, win_rate_pct, lifetime_volume_usdc, trade_count), or any displayable metric via metric:<id> (raw lifetime value) / pctile:<id> (lifetime percentile, direction-corrected so desc is always best-first). Unmeasured values always sort last.
  • na_policy controls wallets missing a weighted metric: exclude (default — only fully-measured wallets qualify) or neutral_50 (missing metrics score a neutral 50th percentile instead).
  • as_of (YYYY-MM-DD) ranks the board as it stood that day, from the daily metric snapshot: lifetime window, the snapshotted metrics and the filters the snapshot can answer. Anything else is a 400 with the reason — never a live board. The metric-windows answer above lists exactly what as_of honours.
  • explain: true returns how many wallets each gate dropped.
  • The same request works unchanged on the x402 agent rail (POST https://api-agent.polyrank.app/v1/agent/rankings/custom, pay per call, no account) and through the MCP server's polyrank_rank_wallets tool (with your API key). The Builder's goals — "Copy entries", "Copy engine", … — are published as ready-to-send bodies at GET /v1/agent/rankings/goals (free).
  • Full config schema: API Reference → Rankings.

Reading the breakdown (the measured marker)

Set include_breakdown: true and each ranking row carries a breakdown object — the per-metric percentile that fed the composite. Two response fields work together:

FieldMeaning
breakdown{ metricId → percentile }. A cell is null when the metric was not measured for that wallet (below the metric's minimum sample size, or no activity in the requested window).
breakdown_measured{ metricId → boolean }. false mirrors a null cell — the metric is unmeasured. Present only when include_breakdown is set.

A null cell is not a zero

A windowed percentile join-miss (a wallet with no row in the 30-day table) serializes to null over the wire — not a real 0 or P50. Reading it as a measured 0 silently corrupts a re-derived composite. Always branch on breakdown_measured[id]:

  • false → render — (unmeasured). Under na_policy: neutral_50 the composite scored it as a neutral P50; under exclude the wallet was dropped if this metric was weighted.
  • true → the percentile in breakdown[id] is a real measured value (even if it happens to be 0).

Recipes to start from

GoalWeights sketch
Pure forecasterbrier 0.5 · ece 0.3 · information_coefficient 0.2
Copyable sharpalpha_vs_mid 0.4 · brier 0.3 · late_entry_alpha 0.3
Steady compoundersharpe 0.4 · max_drawdown_pct 0.3 · profit_factor 0.3

On this page