English

See Dollar · 给 Agent 的接口

See Dollar 是一个 AI token 速度测试:交上你最近 30 天的编码 Agent 用量,拿到一个可核对的成绩、一个公开名次,和一份能分享的用量画像。

你是人的话,一步一步的中文说明在 /how。这一页是给机器读的。

Schema 版本 0.3.0. 机器可读: /.well-known/seedollar.json - 压缩版: /llms.txt

下面的规范部分(端点表、字段名、hash scope、错误码)只有英文。它们与 /llms.txt、/.well-known/seedollar.json 由同一份源渲染 —— 再翻一份中文就是第二份要同步维护的规范,而规范漂移正是这个项目已经栽过两次的地方。

提交一次成绩

  1. Collect on your human's machine: npx ccusage@20.0.19 daily --json --by-agent > usage.json . This is not fully offline: the first run downloads ccusage from npm, and ccusage may fetch a public model price list while pricing; neither request carries usage data. usage.json stays on the machine: never upload, paste, print or edit it.
  2. Scrub deterministically: download /assets/seedollar-scrub.mjs (exact versioned URL and sha256 in "The deterministic scrubber" below), check its sha256, then run node seedollar-scrub.mjs usage.json -o seedollar-report.json. It makes no network calls and writes a schema 0.3.0 report with evidence_source "local_scan". Do not write, convert or edit the report yourself. If seedollar-scrub.mjs exits non-zero, no report was written: stop and show your human its message. Never open, print, edit, filter or rewrite usage.json, and never write a report by hand - that would be you changing the numbers. Your human decides whether to re-collect with the pinned ccusage command.
  3. Show your human the summary the scrubber printed and the report file, and wait for an explicit OK. No OK, no submit.
  4. Get a ticket. If your human gave you one from /me (test_id + upload_token), use it: the result then lands on their existing identity. Otherwise POST /v1/tests -> { test_id, upload_token, expires_in } - this creates a new anonymous identity each time. A ticket expires 600 seconds after it is issued, so get your own after the OK. A ticket from /me started counting when your human pressed the button: if it has expired by the OK, ask them for a new one.
  5. POST /v1/tests/{test_id}/report with header Authorization: Bearer <upload_token> and the report file as the body, byte-for-byte (curl --data-binary @seedollar-report.json). Any content_sha256 the response (or later the result page) shows must equal the one the scrubber printed.
  6. If step 5 answered status "not_ranked", stop here: the report is stored but nothing in it is rankable, so no rank will ever appear. Otherwise poll GET /v1/tests/{test_id} until rank_status is "ready" (or "failed", or "withheld" = stored but kept off the leaderboard, see rank_withheld_reason). It starts as "pending". Stop polling as well if that response has status "not_ranked". The same response carries facets (per-day rankable volume for the 30 calendar days, active days, model count, five-channel breakdown) frozen at submission.
  7. The result is public at /t/{test_id} (the result_url of the ticket). Share that one: it is pinned to this submission and never changes. /r/{public_id} shows the subject current result and moves on every retest.
# 1 - collect on your human's machine (npx downloads ccusage on first run)
npx ccusage@20.0.19 daily --json --by-agent > usage.json

# 2 - scrub deterministically (no network; never edit what it writes; if it exits non-zero, stop and show your human)
# macOS / Linux / Git Bash:
curl -fsSL -o seedollar-scrub.mjs "https://seedollar.studio/assets/seedollar-scrub.mjs?v=dcef6944"
# Windows cmd / PowerShell (in PowerShell 5.1 a bare curl is Invoke-WebRequest):
curl.exe -fsSL -o seedollar-scrub.mjs "https://seedollar.studio/assets/seedollar-scrub.mjs?v=dcef6944"
node -e "const h=require('crypto').createHash('sha256').update(require('fs').readFileSync('seedollar-scrub.mjs')).digest('hex');if(h==='dcef6944d9e57d509af0edb2fba913a66c1ebe53c9a18720dafbd3d4564d0862'){console.log('sha256 ok')}else{console.error('sha256 MISMATCH '+h);process.exit(1)}"
node seedollar-scrub.mjs usage.json -o seedollar-report.json

# 3 - show your human the summary and the report file; go on only after an explicit OK

# 4 - a ticket: use the one your human gave you from /me, otherwise
curl -sX POST https://seedollar.studio/v1/tests

# 5 - submit the file byte-for-byte (TOKEN and TEST_ID come from step 4)
curl -sX POST https://seedollar.studio/v1/tests/$TEST_ID/report \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @seedollar-report.json

# 6 - poll until rank_status is ready (or failed / withheld)
curl -s https://seedollar.studio/v1/tests/$TEST_ID

upload_token 是一次性的,600 秒后过期,且只对一个动作有效 —— 它读不了身份,也改不了任何东西。See Dollar 从不需要长期 API key。

确定性剥洗器

不要自己写、转写或修改报告。跑这个文件:它和网页上的剥洗器是同一份代码,报告里的每一个数都是 ccusage 经它产出的。

versionseedollar-scrub 0.3.0
urlhttps://seedollar.studio/assets/seedollar-scrub.mjs?v=dcef6944
sha256dcef6944d9e57d509af0edb2fba913a66c1ebe53c9a18720dafbd3d4564d0862
downloadcurl -fsSL -o seedollar-scrub.mjs "https://seedollar.studio/assets/seedollar-scrub.mjs?v=dcef6944"
curl.exe -fsSL -o seedollar-scrub.mjs "https://seedollar.studio/assets/seedollar-scrub.mjs?v=dcef6944" (Windows cmd / PowerShell)
verifynode -e "const h=require('crypto').createHash('sha256').update(require('fs').readFileSync('seedollar-scrub.mjs')).digest('hex');if(h==='dcef6944d9e57d509af0edb2fba913a66c1ebe53c9a18720dafbd3d4564d0862'){console.log('sha256 ok')}else{console.error('sha256 MISMATCH '+h);process.exit(1)}"
sha256sum seedollar-scrub.mjs · shasum -a 256 seedollar-scrub.mjs · certutil -hashfile seedollar-scrub.mjs SHA256 · Get-FileHash seedollar-scrub.mjs -Algorithm SHA256
Compare the hex they print with sha256 above. Get-FileHash prints it in uppercase: compare ignoring case.
runnode seedollar-scrub.mjs usage.json -o seedollar-report.json

人给你的那张票

你的人类被告知该粘给你的那段话

如果有人把下面某一段粘给了你,这就是它的全文。

Go to https://seedollar.studio/llms.txt and follow it to submit my last 30 days of coding-agent token usage to See Dollar.
Read https://seedollar.studio/llms.txt and submit my last 30 days of coding-agent token usage to See Dollar.
Every number must come from the tools, never from you:
  1. run: npx ccusage@20.0.19 daily --json --by-agent > usage.json
  2. download seedollar-scrub.mjs from the URL in llms.txt, check its sha256, then run: node seedollar-scrub.mjs usage.json -o seedollar-report.json
  3. show me the summary it prints and the report file, then wait for my OK;
  4. after I say OK, submit the file byte-for-byte - do not edit, round or retype any number.
If the scrubber exits non-zero, stop and show me its message - never open, edit or rewrite usage.json and never write a report yourself.
Never upload or paste usage.json anywhere. If the server answers 400, show me the issues and re-run the scrubber - do not patch the JSON.

/me 上的按钮会把三个值填进去;下面是带占位符的同一段话。

Read https://seedollar.studio/llms.txt and submit my last 30 days of coding-agent token usage to See Dollar with this ticket - it keeps my existing See Dollar identity:
  test_id: <test_id>
  upload_token: <upload_token>
  (single use, valid until <expires_at, UTC> - its 10 minutes started when I pressed the button)
Do NOT call POST /v1/tests: that would put the result on a second identity. Do steps 1-3 first and use the ticket only after my OK; if it has expired by then, ask me for a new one.
Every number must come from the tools, never from you:
  1. run: npx ccusage@20.0.19 daily --json --by-agent > usage.json
  2. download seedollar-scrub.mjs from the URL in llms.txt, check its sha256, then run: node seedollar-scrub.mjs usage.json -o seedollar-report.json
  3. show me the summary it prints and the report file, then wait for my OK;
  4. after I say OK, POST the file byte-for-byte to https://seedollar.studio/v1/tests/<test_id>/report with header Authorization: Bearer <the upload_token above>.
If the scrubber exits non-zero, stop and show me its message - never open, edit or rewrite usage.json and never write a report yourself.
Never upload or paste usage.json anywhere. If the server answers 400, show me the issues and re-run the scrubber - do not patch the JSON.

端点表

MethodPathAuthWhat it doesAgent
POST /v1/tests none or Authorization: Dollar <client_token> (Dollar desktop app only) Start a speedtest. Returns a one-time upload_token and (for new visitors) creates an anonymous subject.
  • No request body required.
  • Response: { test_id, upload_token, expires_in, schema_version, result_url, subject_url, subject_public_id, subject_created }. Share result_url (/t/, pinned); subject_url (/r/) moves on every retest.
  • Browsers additionally receive an httpOnly subject cookie. Agents do not, and must not ask a user for one.
  • Dollar (desktop app) sends Authorization: Dollar <client_token> and gets a ticket for the subject that issued that token; no cookie is set.
  • Agents: without a credential this creates a NEW anonymous identity every time. If your human already has a See Dollar identity, ask them for a ticket from /me (POST /v1/subjects/me/agent-ticket) and skip this call - the result then lands on their existing identity.
yes
POST /v1/tests/{test_id}/report Authorization: Bearer <upload_token> or browser subject cookie or Authorization: Dollar <client_token> (Dollar desktop app only) Submit a usage report for this test. This is the only write endpoint an agent ever needs.
  • Header: Authorization: Bearer <upload_token>. This is the path agents use.
  • Body: a schema 0.3.0 report object (see fields below). Send the report and nothing else.
  • Agents: the body is the file seedollar-scrub.mjs wrote, sent byte-for-byte (curl --data-binary @seedollar-report.json) after your human said OK. Never build, convert or edit the JSON yourself.
  • An accepted / unchanged response carries content_sha256: it must equal the one seedollar-scrub.mjs printed. If it does not, tell your human.
  • evidence_source must be "local_scan" for now. The other values in the schema are planned but cannot be stored yet; they get 400 unsupported_evidence_source.
  • Response (202): status is "accepted" or "unchanged" (same content resubmitted) with the rank fields, or "not_ranked" when no row in the window is rankable (stored, not on the leaderboard; nothing to poll).
  • The token is scoped to this test_id and expires after 600 seconds. It cannot read or modify anything.
  • A rejected report does NOT burn the token: re-run seedollar-scrub.mjs and resubmit its output with the same ticket within the 600 seconds (independent implementations: fix your implementation).
  • Do NOT put submission_path in the report. It does not exist in the schema. The server records how you got here (Bearer token => agent, browser cookie => web_upload, Dollar client token => dollar) from your credential, never from the body.
  • A stored report can still be kept off the leaderboard: the response then has status "accepted", rank_status "withheld", and rank_withheld_reason + rank_withheld_message (machine code + human sentence; both are null otherwise). Reasons: "duplicate_of_existing_subject" (the same content_sha256 is already the current result of another identity; the first submission counts, so do not resubmit the same file from a fresh ticket/identity), "quarantined" (outlier review pending; enters the leaderboard once cleared), "quarantine_rejected".
yes
GET /v1/tests/{test_id} none Poll a test for its result. Returns rank_status = pending until the rank has been computed.
  • status first: "awaiting_report" = the ticket exists but no report was submitted yet; "not_ranked" = the report is stored but had no rankable rows, so there will never be a rank - STOP polling; "ready_or_pending" = a result exists, branch on rank_status.
  • rank_status is the ONLY field you branch on for the rank. Never infer state from rank_at_ingest being null.
  • rank_status is one of pending | ready | failed | withheld. Stop polling on ready, failed or withheld (withheld = stored and visible to you, not on the leaderboard; see rank_withheld_reason / rank_withheld_message).
  • rank_at_ingest is null while rank_status is pending, failed or withheld. The field is always present.
  • stale (read-only): true when window_to is more than 3 days before today (UTC). A result covers the 30 days ending on its window_to, not "the last 30 days"; it is frozen at submission and does not roll. Stale results stay on the leaderboard, marked. Submit a fresh report to refresh.
  • facets (frozen at submission): by_day[30], active_days, models, breakdown, rankable_channels. null for results older than facets.
yes
GET /v1/leaderboard none The public 30-day token leaderboard.
  • Query: window (only "30d" is valid; anything else is a 400), limit (default 50, max 200), offset.
  • Query: public_id=<10 chars> returns just that subject’s row, including its rank.
  • Query: region=<ISO 3166-1 alpha-2> keeps only subjects that self-declared that region. Ranks stay global (they are not re-numbered); an unknown code is a 400 bad_region.
  • Ranking metric is rankable_token_volume_v1. evidence_source and submission_path are shown but never sorted on.
  • Each entry carries window_from / window_to (the 30 days that score covers) and stale (true when window_to is more than 3 days before today, UTC). Stale entries are NOT removed or re-ranked; they are only marked.
  • One report counts once: if the same content_sha256 is the current result of several identities, only the earliest submission is on the leaderboard. Withheld results (duplicate / under outlier review) are not on it.
yes
GET /v1/leaderboard/regions none How many leaderboard entries self-declared each region. Aggregate only; small counts are masked.
  • Query: window (only "30d" is valid; anything else is a 400).
  • Counts the same set as /v1/leaderboard (withheld, retired and duplicate-content results are not on it), so total equals /v1/stats subjects_ranked.
  • Response: { window, total, regions: [{ region, count }], unknown, generated_at }. count is a number, or "lt3" when fewer than 3 entries declared that region. Numeric regions come first (count desc, then code); "lt3" regions follow in code order, so the order does not reveal 1 vs 2.
  • unknown = entries that declared no region: "lt10" when fewer than 10, otherwise { count, approx: true } with count rounded DOWN to a multiple of 10.
  • region is the self-declared ISO 3166-1 alpha-2 code set on /me. It is never inferred from IP or headers, and entries without one are never placed on a map.
  • Public and cacheable: Cache-Control public, max-age=60.
yes
GET /v1/stats none Site-wide counters for the 30-day window.
  • Returns subjects_ranked and total_rankable_token_volume_v1 over the same comparison set the leaderboard uses.
  • total_rankable_token_volume_v1 is the sum of the results currently on the leaderboard. Each result covers the 30 days ending on its own window_to (some may be stale), so the total is NOT "tokens in the last 30 days".
  • Retired subjects are excluded, so these counters never double-count one person who started fresh.
yes
GET /v1/hit/stats none Anonymous share-funnel counters: how many times each whitelisted share / page event happened per UTC day.
  • Query: days (default 30, max 90). Anything else is a 400 bad_days.
  • Response: { from, to, days: [{ day, counts: { <event>: n } }], totals, events, note, generated_at }. Every day in the range is listed; a day with no events has counts {}.
  • Counts are clicks / page views, not people. Nothing behind them is stored: no id, IP, user agent or cookie. They are written by See Dollar pages when a visitor shares or follows a shared link; agents have nothing to write here.
  • Public and cacheable: Cache-Control public, max-age=60.
yes
GET /v1/subjects/me browser subject cookie or Authorization: Dollar <client_token> (Dollar desktop app only) Your own current result and rank. Requires the browser subject cookie (or a Dollar client token).
  • An upload_token is always rejected here with 401. A ticket can write one score; it can never read an identity.
  • Response also carries rank_delta: previous rank_at_ingest minus current, only when both revisions are ready; otherwise null. Dollar shows it as the arrow next to MY SEE DOLLAR POSITION.
  • Agents: use GET /v1/leaderboard?public_id=<your public id> instead. It needs no credential.
  • current carries stale and rank_withheld_reason / rank_withheld_message like GET /v1/tests/{test_id}; leaderboard_entry is null while the current result is withheld.
no - needs a browser cookie
POST /v1/subjects/me/reset browser subject cookie NOT ME / START FRESH. Retires the current subject and clears the cookie.
  • There is no account and no recovery. Once retired, the old public_id cannot be recovered.
  • Already-shared result links keep working. The retired score leaves the leaderboard.
no - needs a browser cookie
POST /v1/subjects/me/profile browser subject cookie Set or clear your display name and self-declared region. Browser cookie only.
  • Body fields are independent and optional: { display_name: "Sam_L" | null, region: "SG" | null }. Absent field = unchanged; null = clear.
  • display_name: 3-20 chars of [A-Za-z0-9_-], globally unique (case-insensitive), reserved words rejected. Display only — it never becomes a credential and never affects ranking.
  • region: self-declared ISO 3166-1 alpha-2. Never inferred from IP or headers; display and filtering only.
  • 409 name_taken when the name is already in use. Clearing a name releases it for anyone.
no - needs a browser cookie
POST /v1/subjects/me/agent-ticket browser subject cookie Give my agent a ticket: issue ONE upload ticket bound to this subject, for the human to paste to their agent. Browser cookie only.
  • No request body. Response: { test_id, upload_token, expires_in, expires_at, schema_version, result_url, subject_url, subject_public_id }. The upload_token is shown once.
  • Same ticket as POST /v1/tests: single use, expires after 600 seconds, scoped to POST /v1/tests/{test_id}/report only. It cannot read the identity or issue anything. Issuing one does not cancel earlier tickets; each simply expires.
  • The agent submits with Authorization: Bearer <upload_token>. The server records submission_path "agent" and puts the result on the subject that issued the ticket, so one person keeps one identity across browser and agent.
  • Never creates a subject: without a valid cookie it is 401 no_credential. An upload_token or a Dollar client token is always 401 here.
  • Agents: you cannot call this. If you need a ticket for an existing identity, ask your human to press the button on /me and paste you the text it shows.
no - needs a browser cookie
POST /v1/subjects/me/client-tokens browser subject cookie Connect Dollar: issue a long-lived, revocable client token for this subject. Browser cookie only.
  • Body: { kind: "dollar" }. Response: { client_token, kind, created_at }. The plaintext token is shown exactly once.
  • Paste it into Dollar. Dollar then sends Authorization: Dollar <client_token> to POST /v1/tests, POST /v1/tests/{test_id}/report and GET /v1/subjects/me.
  • A client token can never retire the subject or issue further tokens. Revoke at any time with /v1/subjects/me/client-tokens/revoke.
no - needs a browser cookie
POST /v1/subjects/me/client-tokens/revoke browser subject cookie Disconnect Dollar: revoke every client token of this subject. Browser cookie only.
  • Body: { kind: "dollar" }. Revoked tokens get 401 with reason "revoked" immediately.
no - needs a browser cookie
GET /t/{test_id} none Public page for ONE submission, pinned to the revision it created. This is the default share URL.
  • Immutable: it always shows the result of that submission, at the content_sha256 it was made with.
  • Share this one. /r/{public_id} shows the subject CURRENT result and changes every time they retest.
yes
GET /r/{public_id} none Current result for a subject, by its 10-character public id. Changes on every retest.
  • Accepts both 7F2AC913K5 and the display form 7F2AC-913K5.
  • This is the "see where I rank now" entry point, NOT the share URL. For sharing use /t/{test_id}.
  • JSON: current carries window_from / window_to, stale, rank_status (incl. withheld) and rank_withheld_reason / rank_withheld_message.
yes

报告字段

schema 是严格的 —— 不在这张表里的键一律拒收:

schema_version, report_id, generated_at, evidence_source, snapshot, provenance, tools_detected, rows, totals, taken_at, window, kind, start, end, content_sha256, collector, collector_version, collector_command, scrubber_version, generator, generator_version, estimation_method, date, tool, model_raw, tokens, sessions, client_cost_usd, input, output, cache_read, cache_creation, reasoning

snapshot.content_sha256(内容指纹)

Agents following this file do not compute it: seedollar-scrub.mjs does. This section is for people maintaining an independent, reviewed implementation (CI tooling, a desktop client).

必填,而且服务端会重算。 The server recomputes it and rejects the report on mismatch (rule V12). A guessed or omitted value always fails.

content_sha256 = lowercase_hex(SHA-256(UTF8(JCS(hash_scope))))

hash scope 是白名单 —— 就是下面这些键,多一个少一个都会换一个 hex:

刻意排除的项 —— 把整份报告拿去 hash 一定对不上:

只给独立实现自检用(用 seedollar-scrub.mjs 的 Agent 跳过这一段)。对下面这个报告子集:

{
  "schema_version": "0.3.0",
  "evidence_source": "local_scan",
  "snapshot": {
    "window": {
      "kind": "30d",
      "start": "2026-07-14",
      "end": "2026-08-12"
    }
  },
  "tools_detected": [
    "codex-cli",
    "claude-code"
  ],
  "rows": [
    {
      "date": "2026-08-12",
      "tool": "codex-cli",
      "model_raw": "gpt-5.5",
      "tokens": {
        "input": 5,
        "output": 6,
        "cache_read": 7,
        "cache_creation": 8,
        "reasoning": null
      },
      "sessions": null
    },
    {
      "date": "2026-08-11",
      "tool": "claude-code",
      "model_raw": "claude-opus-5",
      "tokens": {
        "input": 1,
        "output": 2,
        "cache_read": 3,
        "cache_creation": 4,
        "reasoning": null
      },
      "sessions": null
    }
  ]
}

规范化 JSON 是:

{"domain":"seedollar-snapshot-v1","evidence_source":"local_scan","rows":[{"date":"2026-08-11","model_raw":"claude-opus-5","sessions":null,"tokens":{"cache_creation":4,"cache_read":3,"input":1,"output":2,"reasoning":null},"tool":"claude-code"},{"date":"2026-08-12","model_raw":"gpt-5.5","sessions":null,"tokens":{"cache_creation":8,"cache_read":7,"input":5,"output":6,"reasoning":null},"tool":"codex-cli"}],"schema_version":"0.3.0","tools_detected":["claude-code","codex-cli"],"window":{"from":"2026-07-14","kind":"30d","to":"2026-08-12"}}

而 content_sha256 必须是 9e64e7b84c83f8f61d252b40cb12e32aa54576068ef54d0f7b38b07ba4ab478a. 独立实现复现不出那个 hex,先改实现再提交 —— 服务端会拒收。Agent:绝不为了让 hash 对上去改报告,重跑剥洗器。

提交失败时

HTTPerrorwhenfix
400schemaA field is missing, has the wrong type, or breaks a cross-field rule. This includes window.kind="30d" must span exactly 30 calendar days inclusive, and every row.date must fall inside the window. issues[] names each violation with its path.If you used seedollar-scrub.mjs: do not edit the file. Re-download the scrubber (check the sha256 above), re-run it, show your human the new summary and POST the new file with the same ticket. Independent implementations: fix what issues[] lists. A schema failure does NOT consume the upload_token.
400validationThe report parsed but a validator rule rejected it. BY FAR the most common one: your content_sha256 does not match what the server recomputed (rule V12) - issues[0] then shows both hexes. NOTE: this is reported as error="validation", NOT as "content_sha256_mismatch".If you used seedollar-scrub.mjs, the file was changed after the scrubber wrote it: re-run the scrubber and submit its output unchanged. Independent implementations: reproduce the self-check vector until your hex matches. Also does NOT consume the ticket.
401unauthorizedThe reason field says which: no_credential (no Authorization header), already_used (the ticket was already consumed by a successful submit), expired (older than the TTL), not_your_test.If your human gave you the ticket from /me, ask them for a new one - do not call POST /v1/tests, that is a different identity. Otherwise POST /v1/tests for a fresh ticket. Tickets are single-use and short-lived.
429rate_limitedToo many POST /v1/tests per hour from one IP without any credential.Wait for the next hour. Reusing a credential you already hold is not rate limited.
413payload_too_largeThe request body is larger than 2 MiB. Checked while streaming, before the body is parsed.A 30-day report is a few tens of KB. A body this large is almost certainly not a report - check what you are sending.
400unsupported_evidence_sourceThe report parsed, but its evidence_source is not one the server can store yet. Accepted today: local_scan. agent_generated and user_estimated are planned (the schema already has them) but cannot be stored yet.Submit the report seedollar-scrub.mjs wrote (it is always local_scan, rows produced by the collector named in provenance; see the ranking scope note below). Does NOT consume the ticket - resubmit the right file with the same upload_token.
400unsupported_windowA window= query value other than 30d.Only 30d exists. The server never silently falls back to it.

Rows outside the server window do not fail the submit - they are stored and simply not counted. A report entirely outside it returns 202 with rankable_token_volume_v1 = 0, not an error. The ranking window is anchored to the real today, not to the window declared in your report.

Only rows that match a semantics profile count toward rankable_token_volume_v1: tool claude-code, codex-cli, gemini-cli [draft profile: results are labelled PROFILE: DRAFT] collected by ccusage >=20.0.0 <21.0.0 (`ccusage daily --json --by-agent`). Rows from any other tool, collector or collector version are stored but not counted. If no row inside the 30-day window counts, the submit returns 202 with status "not_ranked" and rankable_token_volume_v1 = null.

不要做的事