{"openapi":"3.1.0","info":{"title":"ProbNode API","summary":"Tick-level prediction-market data: catalog, coverage ledger and delivery","description":"\nTick-level order books and trades for prediction markets, with a published record of what was and\nwas not captured.\n\nThe tape is recorded separately (`pandora-poly-monitor`): every change of the book for every market,\nmillisecond timestamps from both the exchange and our receiver, snapshots marked, and every\nwebsocket outage logged. This API is how that tape is catalogued and handed out.\n\n## Product coverage\n\nWhat the four product pillars are, and what this API version actually serves.\n\n| Pillar | Capability | This version |\n|---|---|---|\n| **Market data** | catalog, coverage ledger, the tape itself | **implemented** |\n| **Market analysis** | replay of any market tick by tick, series digests, hints | planned |\n| **Player analysis** | who traded on this tick, per-wallet execution quality | planned |\n| **Strategy analysis** | rules over book state, equity curve, code export | planned |\n\n## Authentication\n\nEvery write and every paid read takes an API key in the `Authorization` header:\n\n```\nAuthorization: Bearer pmd_<prefix>_<secret>\n```\n\nRegister with `POST /v1/auth/register` and the key comes back once - it is stored only as a hash and\nis never returned again. `POST /v1/auth/login` mints another. Keys are listed by prefix and revoked\nat `/v1/keys`.\n\nGoogle sign-in is the same account by another door. `GET /v1/auth/google/start` redirects to\nGoogle's consent screen (authorization code + PKCE, with a signed single-use `state` bound to an\nHttpOnly cookie), and `GET /v1/auth/google/callback` verifies the ID token against Google's JWKS -\nissuer, audience, expiry and signature - then redirects to the web app with a freshly minted key in\nthe URL *fragment*, which browsers do not send to servers. A verified email that already has a\npassword account links to that account; it never opens a second one. Both are browser redirects\nrather than JSON endpoints, so there is nothing here for a client library to call.\n\nRegistration sends a link to confirm the address; the web app redeems it with\n`POST /v1/auth/verify`, and `POST /v1/auth/verify/resend` sends another (one a minute). A forgotten\npassword is `POST /v1/auth/password/forgot`, which answers the same for every address so it cannot\nbe used to find out who is registered, then `POST /v1/auth/password/reset` with the link's token. A\nreset link works for an hour and only once, and using it also confirms the address. Both links carry\ntheir token in the URL fragment. A Google sign-in confirms the address by itself.\n\nThe catalog and the coverage ledger need no key at all. That is deliberate: the ledger is the thing\na researcher has to be able to cite before they buy anything.\n\n## Plans and entitlements\n\nFour separate concepts, kept apart on purpose:\n\n* **entitlement** - which dataset surfaces a plan may touch (`samples`, `tape`);\n* **quota** - how many signed URLs a day, checked against the meter before anything is signed;\n* **rate limit** - requests a minute (declared per plan; enforcement is not in this version);\n* **meter** - the idempotent ledger consumption is billed and counted from.\n\n| Plan | Datasets | Signed URLs / day | Requests / min |\n|---|---|---|---|\n| `free` | samples, catalog | 50 | 60 |\n| `research` | + the whole tape | 20,000 | 600 |\n| `pro` | + the whole tape | 200,000 | 3,000 |\n| `academic` | + the whole tape | 20,000 | 600 |\n\n`academic` is granted by hand through the admin API, in exchange for a citation.\n\n## Delivery\n\nBulk data never streams through this API. `POST /v1/data/urls` returns short-lived signed URLs over\nobject storage, and records both a meter event and an audit row for every issuance. Checks run in\nthis order - **entitlement, then quota, then sign** - so nothing is ever signed and then refused,\nand a batch containing one refused path is refused whole.\n\nObject paths are relative paths inside the lake, for example\n`ds/a1/v2/books_lp/event_date=2026-11-08/hour=12/part-0.lp2`. Anything absolute, with `..`, or with\na backslash is rejected with 422.\n\n## Errors\n\n| Code | Means |\n|---|---|\n| 401 | missing, malformed or revoked API key |\n| 402 | the plan does not cover that dataset |\n| 403 | admin-only endpoint |\n| 404 | no such market, user or key |\n| 422 | the request body or an object path is invalid |\n| 429 | the daily signed-URL quota is exhausted |\n| 503 | the database is unreachable (`/ready` only) |\n","contact":{"name":"ProbNode"},"license":{"name":"Proprietary"},"version":"0.1.0"},"servers":[{"url":"https://api.pandora-market-data.example","description":"production"},{"url":"http://localhost:8000","description":"local development"}],"paths":{"/health":{"get":{"tags":["health"],"summary":"Liveness","operationId":"health_live","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health Live"}}}}}}},"/ready":{"get":{"tags":["health"],"summary":"Readiness (touches the database)","operationId":"health_ready","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health Ready"}}}}}}},"/v1/auth/register":{"post":{"tags":["auth"],"summary":"Register an account","description":"Open an account and send a link to confirm the address.\n\nThe account and its key do not wait on the mail relay: if the email cannot be sent the\nregistration still stands, `verification_email_sent` is false, and the link can be asked for\nagain with `POST /v1/auth/verify/resend`.","operationId":"auth_register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"The new account on the free plan, its first key, and whether the verification email went out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterResponse"}}}},"409":{"description":"That email is already registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/verify":{"post":{"tags":["auth"],"summary":"Confirm an email address","description":"Redeem the token from a verification link. Needs no key: the link is the proof.","operationId":"auth_verify_email","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailRequest"}}},"required":true},"responses":{"200":{"description":"The account, with `email_verified` true. Opening the same link again returns the same.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserOut"}}}},"400":{"description":"The link is malformed, expired, or for an address the account no longer has.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/verify/resend":{"post":{"tags":["auth"],"summary":"Send the verification email again","operationId":"auth_resend_verification","responses":{"202":{"description":"The email was handed to the mail relay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailSent"}}}},"401":{"description":"No key, or a key that is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This address is already verified.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"A verification email went to this account less than a minute ago.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"The mail relay refused the message or could not be reached. Try again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/v1/auth/password/forgot":{"post":{"tags":["auth"],"summary":"Ask for a password reset link","description":"Email a reset link to the address, if it has an account with a password.\n\nAn account opened with Google has no password to reset; it gets an email saying to sign in\nwith Google instead, so the person is not left waiting for a link that will never come. An\naddress with no account gets nothing. The mail is sent after the response, so neither the\nstatus nor the timing differs between those three cases.","operationId":"auth_forgot_password","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordRequest"}}},"required":true},"responses":{"202":{"description":"Always the same answer, whether or not the address has an account, so this endpoint cannot be used to find out who is registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailSent"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/password/reset":{"post":{"tags":["auth"],"summary":"Choose a new password","description":"Set a new password with the token from a reset link, and mark the address verified.\n\nThe link only ever reached the inbox, so using it proves the address as well as a\nverification link would. It works once: the new password hash no longer matches the\nfingerprint the token carries.","operationId":"auth_reset_password","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"}}},"required":true},"responses":{"200":{"description":"A new API key named `reset`, so the person is signed in. Keys made before keep working until revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyIssued"}}}},"400":{"description":"The link is malformed, expired, or has already been used. Ask for a new one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/login":{"post":{"tags":["auth"],"summary":"Sign in and mint a key","operationId":"auth_login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"A new API key. The previous ones keep working until revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyIssued"}}}},"401":{"description":"Wrong email or password, or an account with no password.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/google/start":{"get":{"tags":["auth"],"summary":"Begin Google sign-in","description":"Start the authorization-code flow with PKCE.\n\nOpen this in the browser's address bar, not with `fetch`: it is a redirect to Google, and the\ncookie it sets is what makes the callback verifiable.","operationId":"auth_google_start","responses":{"307":{"description":"A redirect to Google's consent screen, plus the short-lived HttpOnly cookie that the callback checks the `state` against."}}}},"/v1/auth/google/callback":{"get":{"tags":["auth"],"summary":"Finish Google sign-in","description":"Google's redirect target. Verifies everything, then mints a key for the account.\n\nTwo kinds of failure, treated differently on purpose. A flow that cannot be trusted - no\ncookie, a forged `state`, no `code` - gets a 400 with the reason, because feeding it into\naccount creation is exactly what an attacker wants. A flow that is genuine but did not succeed\n- consent refused, Google unreachable, an unverified address - is handed back to the web app\nwith the reason in the fragment, so the person reads it on the sign-in page.","operationId":"auth_google_callback","parameters":[{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}}],"responses":{"307":{"description":"A redirect to the web app at `/auth/google`, carrying either `#key=...` or `#error=...` in the fragment. The fragment keeps the key out of logs and referrers."},"400":{"description":"The callback could not be trusted: no flow cookie, a `state` that does not match it, or a missing `code`. Start again from the sign-in page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me":{"get":{"tags":["auth"],"summary":"The current account","operationId":"auth_me","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserOut"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/v1/keys":{"get":{"tags":["auth"],"summary":"List keys","operationId":"auth_list_keys","security":[{"ApiKeyBearer":[]}],"responses":{"200":{"description":"Keys by prefix. The secret half is never returned again.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyOut"},"title":"Response Auth List Keys"}}}}}},"post":{"tags":["auth"],"summary":"Mint a key","operationId":"auth_create_key","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"name","in":"query","required":false,"schema":{"type":"string","default":"key","title":"Name"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyIssued"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/keys/{prefix}":{"delete":{"tags":["auth"],"summary":"Revoke a key","operationId":"auth_revoke_key","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"prefix","in":"path","required":true,"schema":{"type":"string","title":"Prefix"}}],"responses":{"204":{"description":"Successful Response"},"401":{"description":"No key, or a key that is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No key with that prefix on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/catalog/series":{"get":{"tags":["catalog"],"summary":"List series","description":"A series is a recurring family of markets, such as the 5-minute Bitcoin up/down market.\n\nPublic: no key. Use the slug to filter `/v1/catalog/markets`, `category` to narrow to one\npart of the taxonomy and `q` to search slugs and titles.","operationId":"catalog_list_series","parameters":[{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/CategoryCode"},{"type":"null"}],"description":"Narrow to one category.","title":"Category"},"description":"Narrow to one category."},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Substring of the slug or the title.","title":"Q"},"description":"Substring of the slug or the title."}],"responses":{"200":{"description":"Every recurring family of markets we record.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SeriesOut"},"title":"Response Catalog List Series"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/catalog/categories":{"get":{"tags":["catalog"],"summary":"The category taxonomy","description":"The whole taxonomy, including the parts we do not record yet.\n\nA category with `series_count` of zero is not an error: it is us saying plainly that nothing\nthere is captured, which is the same rule the coverage ledger follows.","operationId":"catalog_list_categories","responses":{"200":{"description":"Every category, with how much of it we actually record.","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CategoryOut"},"type":"array","title":"Response Catalog List Categories"}}}}}}},"/v1/catalog/markets":{"get":{"tags":["catalog"],"summary":"List markets","description":"Markets, newest first, each carrying its tokens.\n\nA market has one token per outcome; the token id is what the tape and the coverage ledger are\nkeyed by. `resolution` is null while the market is still open.","operationId":"catalog_list_markets","parameters":[{"name":"series","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to one series slug.","title":"Series"},"description":"Restrict to one series slug."},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/CategoryCode"},{"type":"null"}],"description":"Narrow to one category.","title":"Category"},"description":"Narrow to one category."},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Substring of the market slug.","title":"Q"},"description":"Substring of the market slug."},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/MarketStatus"},{"type":"null"}],"description":"Only open markets, or only resolved ones.","title":"Status"},"description":"Only open markets, or only resolved ones."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Page size.","default":100,"title":"Limit"},"description":"Page size."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Rows to skip.","default":0,"title":"Offset"},"description":"Rows to skip."}],"responses":{"200":{"description":"Markets newest first, each with its tokens.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MarketOut"},"title":"Response Catalog List Markets"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/catalog/markets/{slug}":{"get":{"tags":["catalog"],"summary":"Get one market","description":"One market by slug, with its tokens and its resolved outcome if it has one.","operationId":"catalog_get_market","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketOut"}}}},"404":{"description":"No market with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/catalog/coverage":{"get":{"tags":["catalog"],"summary":"Coverage ledger","description":"What we recorded for a token, hour by hour. A hole is reported, never hidden.\n\n`recorded_ms` counts the milliseconds inside the hour for which the book was being captured,\n`gap_count` the number of websocket outages, and `uptime_pct` is the first over 3,600,000.\nPublic: no key, because a dataset nobody can inspect is a dataset nobody can cite.","operationId":"catalog_get_coverage","parameters":[{"name":"token_id","in":"query","required":true,"schema":{"type":"string","title":"Token Id"}},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start"}},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End"}}],"responses":{"200":{"description":"One row per recorded hour, with gaps counted.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CoverageHourOut"},"title":"Response Catalog Get Coverage"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/data/urls":{"post":{"tags":["data"],"summary":"Sign download URLs","description":"Hand out short-lived URLs for objects in the lake.\n\nChecks run in this order - **entitlement, then quota, then sign** - so nothing is signed and\nthen refused, and a batch with one refused path is refused whole. Every issuance writes an\nidempotent meter event and an audit row; retrying the same path within the same minute counts\nonce but is audited every time.","operationId":"data_sign_urls","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUrlsRequest"}}},"required":true},"responses":{"200":{"description":"One short-lived signed URL per requested object path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUrlsResponse"}}}},"401":{"description":"No key, or a key that is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"An object path is not a plain relative path inside the lake.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"The plan does not cover one of the requested datasets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An hour is newer than this plan reads - see the freshness cutoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"The daily signed-URL quota is exhausted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/v1/data/usage":{"get":{"tags":["data"],"summary":"Usage and quota","description":"What was used, what the cap is, and when it resets - read from the meter, not from logs.","operationId":"data_get_usage","responses":{"200":{"description":"What this account used today, against what its plan allows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageOut"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/v1/plans":{"get":{"tags":["billing"],"summary":"Price list","description":"The price list. **Public** - no key.\n\n`founder_monthly_usd` is the launch price, locked for the lifetime of the first accounts;\n`list_monthly_usd` is what stands afterwards. Yearly is `yearly_discount_pct` off. Plans that\nare not `purchasable` are either free or granted by hand (academic, in exchange for a citation).","operationId":"billing_list_plans","responses":{"200":{"description":"Every plan, what it costs and what it entitles.","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PlanOut"},"type":"array","title":"Response Billing List Plans"}}}}}}},"/v1/orders":{"get":{"tags":["billing"],"summary":"My orders","operationId":"billing_list_orders","responses":{"200":{"description":"Orders newest first.","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/OrderOut"},"type":"array","title":"Response Billing List Orders"}}}}},"security":[{"ApiKeyBearer":[]}]},"post":{"tags":["billing"],"summary":"Order a plan","description":"Place an order for a plan. We invoice, then an admin approves and the plan takes effect.\n\nNothing is charged here and no payment details are collected.","operationId":"billing_create_order","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderCreate"}}},"required":true},"responses":{"201":{"description":"The order, pending review.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderOut"}}}},"401":{"description":"No key, or a key that is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Unknown or non-purchasable plan, or unknown term.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This account already has an open order, or is already on that plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ApiKeyBearer":[]}]}},"/v1/orders/{order_id}/cancel":{"post":{"tags":["billing"],"summary":"Cancel a pending order","operationId":"billing_cancel_order","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderOut"}}}},"401":{"description":"No key, or a key that is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such order on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The order has already been decided.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/users":{"get":{"tags":["admin"],"summary":"List accounts","operationId":"admin_list_users","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Accounts, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserOut"},"title":"Response Admin List Users"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/users/{user_id}":{"patch":{"tags":["admin"],"summary":"Change a plan or the admin flag","description":"Move an account to another plan, or grant back-office access.\n\nThe plan code is validated by the schema, not here: `PlanUpdate.plan` is a `PlanCode`, so an\nunknown value is refused with the valid list attached before this function is reached.","operationId":"admin_update_user","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanUpdate"}}}},"responses":{"200":{"description":"The account as it now stands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserOut"}}}},"401":{"description":"No key, or a key that is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated, but not allowed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Unknown plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/orders":{"get":{"tags":["admin"],"summary":"List orders","description":"Orders waiting for a decision, or all of them. This is the sales queue.","operationId":"admin_list_orders","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Orders newest first, with the account that placed them.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminOrderOut"},"title":"Response Admin List Orders"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/orders/{order_id}/approve":{"post":{"tags":["admin"],"summary":"Approve an order","description":"Approve an invoiced order: the buyer's plan changes, so their entitlement changes with it.","operationId":"admin_approve_order","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderDecision"}}}},"responses":{"200":{"description":"The order, approved - the account is on the new plan immediately.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminOrderOut"}}}},"401":{"description":"No key, or a key that is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated, but not allowed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The order has already been decided.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/orders/{order_id}/reject":{"post":{"tags":["admin"],"summary":"Reject an order","operationId":"admin_reject_order","security":[{"ApiKeyBearer":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderDecision"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminOrderOut"}}}},"401":{"description":"No key, or a key that is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated, but not allowed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The order has already been decided.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AdminOrderOut":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"plan":{"type":"string","title":"Plan"},"term":{"type":"string","title":"Term"},"amount_usd":{"type":"integer","title":"Amount Usd","description":"Whole dollars for the term, founder price if it applies."},"founder_price":{"type":"boolean","title":"Founder Price","description":"True when the locked launch price was used."},"provider":{"type":"string","title":"Provider","description":"How it is settled. 'manual' until a payment provider lands."},"status":{"type":"string","title":"Status","description":"pending, approved, rejected or cancelled."},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"decided_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Decided At","description":"Null while the order is still pending."},"user_id":{"type":"string","format":"uuid","title":"User Id"},"user_email":{"type":"string","title":"User Email"}},"type":"object","required":["id","plan","term","amount_usd","founder_price","provider","status","note","created_at","decided_at","user_id","user_email"],"title":"AdminOrderOut","description":"An order as the back office sees it, with the account attached.","examples":[{"amount_usd":29,"created_at":"2026-11-10T18:00:00Z","founder_price":true,"id":"9c1f2d3e-55aa-4b7c-8d9e-1f2a3b4c5d6e","plan":"research","provider":"manual","status":"pending","term":"monthly","user_email":"quant@example.com","user_id":"4a7f0b6e-0b4a-4d1e-9a3c-9f0f3f0f1a2b"}]},"ApiKeyIssued":{"properties":{"key":{"type":"string","title":"Key","description":"The whole key. Shown once and never again."},"prefix":{"type":"string","title":"Prefix","description":"The public half, used to look the key up and to revoke it."},"name":{"type":"string","title":"Name","description":"Whatever you called it, so a list of keys is readable."}},"type":"object","required":["key","prefix","name"],"title":"ApiKeyIssued","description":"A newly minted key. The only time the secret half is ever returned.\n\nWe store a hash, so a lost key is replaced rather than recovered. Save it when you see it.","examples":[{"key":"pmd_7f2a1c_live_s3cr3t-nobody-else-sees","name":"cli","prefix":"7f2a1c"}]},"ApiKeyOut":{"properties":{"prefix":{"type":"string","title":"Prefix"},"name":{"type":"string","title":"Name"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"revoked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked At","description":"Set once revoked; the key stops working at that moment."},"last_used_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Used At","description":"Null until the key is used for the first time."}},"type":"object","required":["prefix","name","created_at","revoked_at","last_used_at"],"title":"ApiKeyOut","description":"A key as it appears in a listing: enough to recognise and revoke it, never the secret.","examples":[{"created_at":"2026-11-08T09:12:00Z","last_used_at":"2026-11-10T18:44:10Z","name":"cli","prefix":"7f2a1c"}]},"CategoryCode":{"type":"string","enum":["politics","elections","geopolitics","economy","finance","crypto","tech","sports","esports","culture","art","weather","science","health","world","mentions"],"title":"CategoryCode","description":"The taxonomy as a type, so a bad category is refused by the validator with the list of\nvalid ones attached, rather than by a hand-written check that has to repeat them."},"CategoryOut":{"properties":{"code":{"type":"string","title":"Code"},"label":{"type":"string","title":"Label"},"blurb":{"type":"string","title":"Blurb"},"series_count":{"type":"integer","title":"Series Count","description":"Series we hold in this category. May be zero."},"market_count":{"type":"integer","title":"Market Count","description":"Markets we hold in this category. May be zero."}},"type":"object","required":["code","label","blurb","series_count","market_count"],"title":"CategoryOut","description":"One part of the taxonomy, with how much of it we actually hold.\n\nCounts can be zero. Showing an empty category is the point: a catalog that hides what it does\nnot have is a catalog nobody can check.","examples":[{"blurb":"Up/down, price thresholds and token-specific questions.","code":"crypto","label":"Crypto","market_count":48213,"series_count":12}]},"CoverageHourOut":{"properties":{"token_id":{"type":"string","title":"Token Id"},"hour_start":{"type":"string","format":"date-time","title":"Hour Start","description":"Start of the hour, UTC."},"recorded_ms":{"type":"integer","title":"Recorded Ms","description":"Milliseconds of the hour we were recording. Max 3600000."},"gap_count":{"type":"integer","title":"Gap Count","description":"How many separate holes. Reported, never hidden."},"uptime_pct":{"type":"number","title":"Uptime Pct","description":"`recorded_ms` as a percentage, for reading at a glance."}},"type":"object","required":["token_id","hour_start","recorded_ms","gap_count","uptime_pct"],"title":"CoverageHourOut","description":"One token-hour of the coverage ledger: what we captured, and what we did not.\n\nThe number nobody else in this field publishes. `recorded_ms` is milliseconds of the hour the\ntoken was actually being recorded, so 3,600,000 is a whole hour; `gap_count` is how many\nseparate holes. Both come from the recorder's own subscription and socket state, not from\ncounting rows - absence of events cannot be told apart from a quiet book by looking at data.","examples":[{"gap_count":1,"hour_start":"2026-11-08T12:00:00Z","recorded_ms":3598200,"token_id":"btc-updown-5m-1762598700-up","uptime_pct":99.95}]},"EmailSent":{"properties":{"sent":{"type":"boolean","title":"Sent"}},"type":"object","required":["sent"],"title":"EmailSent","description":"An email was accepted for sending.\n\nFor a forgotten password this is returned whatever the address, so it says nothing about\nwhether an account exists - that is the point.","examples":[{"sent":true}]},"ErrorResponse":{"properties":{"detail":{"type":"string","title":"Detail","description":"One sentence a human can act on, not a code to look up.","examples":["missing or invalid api key"]}},"type":"object","required":["detail"],"title":"ErrorResponse","description":"What every 4xx and 5xx returns: FastAPI's own `HTTPException` shape, written down.","examples":[{"detail":"missing or invalid api key"}]},"ForgotPasswordRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"ForgotPasswordRequest","description":"Ask for a password reset link. The answer is the same whether or not the account exists.","examples":[{"email":"quant@example.com"}]},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LoginRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","maxLength":128,"minLength":8,"title":"Password"}},"type":"object","required":["email","password"],"title":"LoginRequest","description":"Exchange an email and password for a fresh API key.","examples":[{"email":"quant@example.com","password":"a-long-passphrase"}]},"MarketOut":{"properties":{"slug":{"type":"string","title":"Slug"},"series_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Series Slug","description":"Null for a one-off market with no family."},"start_ts":{"type":"string","format":"date-time","title":"Start Ts"},"end_ts":{"type":"string","format":"date-time","title":"End Ts"},"resolution":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution","description":"Null while the market is still open."},"tokens":{"items":{"$ref":"#/components/schemas/TokenOut"},"type":"array","title":"Tokens"}},"type":"object","required":["slug","series_slug","start_ts","end_ts","resolution","tokens"],"title":"MarketOut","description":"A market with its tokens, and its outcome once it has one.","examples":[{"end_ts":"2026-11-08T12:10:00Z","resolution":"UP","series_slug":"btc-updown-5m","slug":"btc-updown-5m-1762598700","start_ts":"2026-11-08T12:05:00Z","tokens":[{"outcome":"UP","token_id":"btc-updown-5m-1762598700-up"},{"outcome":"DOWN","token_id":"btc-updown-5m-1762598700-down"}]}]},"MarketStatus":{"type":"string","enum":["open","resolved"],"title":"MarketStatus","description":"Whether a market is still trading or has resolved."},"OrderCreate":{"properties":{"plan":{"$ref":"#/components/schemas/PlanCode","description":"Plan code to order. Must be one that is sold."},"term":{"$ref":"#/components/schemas/OrderTerm","description":"Yearly is 25% off and bills twelve months at once.","default":"monthly"},"note":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"Note","description":"Anything we should know, e.g. billing details."}},"type":"object","required":["plan"],"title":"OrderCreate","description":"Place an order. We invoice, an admin approves, and the plan changes then - not here.","examples":[{"plan":"research","term":"monthly"}]},"OrderDecision":{"properties":{"note":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"Note","description":"Kept on the order. Overwrites an earlier note."}},"type":"object","title":"OrderDecision","description":"An admin approving or rejecting an order, with an optional note for the record.","examples":[{"note":"invoice INV-0042 paid 2026-11-10"}]},"OrderOut":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"plan":{"type":"string","title":"Plan"},"term":{"type":"string","title":"Term"},"amount_usd":{"type":"integer","title":"Amount Usd","description":"Whole dollars for the term, founder price if it applies."},"founder_price":{"type":"boolean","title":"Founder Price","description":"True when the locked launch price was used."},"provider":{"type":"string","title":"Provider","description":"How it is settled. 'manual' until a payment provider lands."},"status":{"type":"string","title":"Status","description":"pending, approved, rejected or cancelled."},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"decided_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Decided At","description":"Null while the order is still pending."}},"type":"object","required":["id","plan","term","amount_usd","founder_price","provider","status","note","created_at","decided_at"],"title":"OrderOut","description":"An order and where it has got to. Nothing is charged; this is an invoice flow.","examples":[{"amount_usd":29,"created_at":"2026-11-10T18:00:00Z","founder_price":true,"id":"9c1f2d3e-55aa-4b7c-8d9e-1f2a3b4c5d6e","plan":"research","provider":"manual","status":"pending","term":"monthly"}]},"OrderTerm":{"type":"string","enum":["monthly","yearly"],"title":"OrderTerm","description":"How long an order covers. Yearly bills twelve months at once, at 25% off."},"PlanCode":{"type":"string","enum":["free","research","pro","academic"],"title":"PlanCode","description":"The plan codes, declared rather than derived.\n\nSpelled out on purpose: an enum built at runtime from a dict is invisible to a reader and to\nanything generating a client, and the whole point is that the published document lists the\nvalues. A unit test asserts this stays equal to the price list and the entitlements."},"PlanOut":{"properties":{"code":{"type":"string","title":"Code"},"name":{"type":"string","title":"Name"},"blurb":{"type":"string","title":"Blurb"},"list_monthly_usd":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"List Monthly Usd","description":"Null when the plan is not priced publicly."},"founder_monthly_usd":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Founder Monthly Usd","description":"Locked for the life of the account. Null once the founder window closes."},"yearly_discount_pct":{"type":"integer","title":"Yearly Discount Pct"},"purchasable":{"type":"boolean","title":"Purchasable","description":"False for plans granted by hand, like academic."},"licence":{"type":"string","title":"Licence","description":"Rights rung: what you may do with the data, not how much."},"licence_detail":{"type":"string","title":"Licence Detail"},"datasets":{"items":{"type":"string"},"type":"array","title":"Datasets"},"signed_urls_per_day":{"type":"integer","title":"Signed Urls Per Day"},"requests_per_minute":{"type":"integer","title":"Requests Per Minute"}},"type":"object","required":["code","name","blurb","list_monthly_usd","founder_monthly_usd","yearly_discount_pct","purchasable","licence","licence_detail","datasets","signed_urls_per_day","requests_per_minute"],"title":"PlanOut","description":"A plan as the price page shows it: what it costs, and what it entitles.","examples":[{"blurb":"The whole tape, every market, book and trades, as lp2 and parquet.","code":"research","datasets":["samples","tape"],"founder_monthly_usd":29,"licence":"personal","licence_detail":"Personal use. No redistribution, no resale of the data.","list_monthly_usd":49,"name":"Research","purchasable":true,"requests_per_minute":600,"signed_urls_per_day":20000,"yearly_discount_pct":25}]},"PlanUpdate":{"properties":{"plan":{"$ref":"#/components/schemas/PlanCode","description":"The plan to move this account to."},"is_admin":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Admin","description":"Grant or remove back-office access. Omit to leave unchanged."}},"type":"object","required":["plan"],"title":"PlanUpdate","description":"An admin moving an account to another plan, or granting back-office access.","examples":[{"plan":"academic"}]},"RegisterRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","maxLength":128,"minLength":8,"title":"Password","description":"Eight characters minimum. Stored with Argon2, never in clear."}},"type":"object","required":["email","password"],"title":"RegisterRequest","description":"Open an account. The key comes back once, in the response.","examples":[{"email":"quant@example.com","password":"a-long-passphrase"}]},"RegisterResponse":{"properties":{"user":{"$ref":"#/components/schemas/UserOut"},"api_key":{"$ref":"#/components/schemas/ApiKeyIssued"},"verification_email_sent":{"type":"boolean","title":"Verification Email Sent","description":"False when the verification email could not be sent. The account is open either way; ask for another link with `POST /v1/auth/verify/resend`."}},"type":"object","required":["user","api_key","verification_email_sent"],"title":"RegisterResponse","description":"What registration returns: the account, its first key, and whether the link went out.","examples":[{"api_key":{"key":"pmd_7f2a1c_live_s3cr3t-nobody-else-sees","name":"default","prefix":"7f2a1c"},"user":{"created_at":"2026-11-08T09:12:00Z","email":"quant@example.com","email_verified":false,"id":"4a7f0b6e-0b4a-4d1e-9a3c-9f0f3f0f1a2b","is_admin":false,"plan":"free"},"verification_email_sent":true}]},"ResetPasswordRequest":{"properties":{"token":{"type":"string","title":"Token","description":"Everything after `#token=` in the link. Valid for one hour, and only once."},"password":{"type":"string","maxLength":128,"minLength":8,"title":"Password","description":"Eight characters minimum. Stored with Argon2, never in clear."}},"type":"object","required":["token","password"],"title":"ResetPasswordRequest","description":"Choose a new password with the token from a reset link.","examples":[{"password":"a-new-long-passphrase","token":"eyJ1aWQiOiI0YTdmMGI2ZSJ9.Zx3k1Q.s1gn4ture"}]},"SeriesOut":{"properties":{"slug":{"type":"string","title":"Slug","description":"Stable id. Filter markets with it."},"title":{"type":"string","title":"Title"},"venue":{"type":"string","title":"Venue","description":"Where it trades. Polymarket today."},"category":{"type":"string","title":"Category","description":"One code from the venue's own taxonomy."}},"type":"object","required":["slug","title","venue","category"],"title":"SeriesOut","description":"A recurring family of markets, such as the five-minute Bitcoin up/down market.","examples":[{"category":"crypto","slug":"btc-updown-5m","title":"Bitcoin Up/Down, 5 minute","venue":"polymarket"}]},"SignUrlsRequest":{"properties":{"paths":{"items":{"type":"string"},"type":"array","maxItems":100,"minItems":1,"title":"Paths","description":"Relative object paths inside the lake. Absolute paths, '..' and backslashes are rejected. Prefixes `g/`, `m/`, `manifest/`, `catalog/` and `samples/` need no plan."}},"type":"object","required":["paths"],"title":"SignUrlsRequest","description":"Ask for links to objects in the lake.\n\nA batch with one refused path is refused whole, so a partial result never looks complete.","examples":[{"paths":["b/event_date=2026-11-08/hour=12/src=rec-1/part-000.lp2","manifest/event_date=2026-11-08/hour=12.json"]}]},"SignUrlsResponse":{"properties":{"urls":{"items":{"$ref":"#/components/schemas/SignedUrlOut"},"type":"array","title":"Urls"},"licence":{"type":"string","title":"Licence","description":"The rights rung these bytes are delivered under."},"licence_detail":{"type":"string","title":"Licence Detail","description":"What that rung permits, in one sentence."}},"type":"object","required":["urls","licence","licence_detail"],"title":"SignUrlsResponse","description":"One signed URL per requested path, in the order they were asked for.","examples":[{"licence":"personal","licence_detail":"Personal use. No redistribution, no resale of the data.","urls":[{"expires_at":"2026-11-10T19:15:00Z","object_path":"manifest/event_date=2026-11-08/hour=12.json","url":"https://lake.example/pb-monitor-lake/manifest/..."}]}]},"SignedUrlOut":{"properties":{"object_path":{"type":"string","title":"Object Path"},"url":{"type":"string","title":"Url","description":"Fetch it directly. Expires - ask again rather than caching it."},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"type":"object","required":["object_path","url","expires_at"],"title":"SignedUrlOut","description":"One short-lived link to one object, plus when it stops working.","examples":[{"expires_at":"2026-11-10T19:15:00Z","object_path":"b/event_date=2026-11-08/hour=12/src=rec-1/part-000.lp2","url":"https://lake.example/pb-monitor-lake/b/event_date=2026-11-08/..."}]},"TokenOut":{"properties":{"token_id":{"type":"string","title":"Token Id"},"outcome":{"type":"string","title":"Outcome"}},"type":"object","required":["token_id","outcome"],"title":"TokenOut","description":"One outcome of a market. The token id is what the tape and the ledger are keyed by.","examples":[{"outcome":"UP","token_id":"btc-updown-5m-1762598700-up"}]},"UsageOut":{"properties":{"plan":{"type":"string","title":"Plan"},"datasets":{"items":{"type":"string"},"type":"array","title":"Datasets","description":"Dataset surfaces this plan may touch at all."},"signed_urls_used_today":{"type":"integer","title":"Signed Urls Used Today"},"signed_urls_per_day":{"type":"integer","title":"Signed Urls Per Day"},"requests_per_minute":{"type":"integer","title":"Requests Per Minute"},"resets_at":{"type":"string","format":"date-time","title":"Resets At","description":"When the daily count goes back to zero, UTC."},"freshness":{"type":"string","title":"Freshness","description":"How close to now this plan reads, in three words."},"freshness_cutoff":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Freshness Cutoff","description":"The first hour this plan may not read yet. Null when only samples apply."}},"type":"object","required":["plan","datasets","signed_urls_used_today","signed_urls_per_day","requests_per_minute","resets_at","freshness","freshness_cutoff"],"title":"UsageOut","description":"What this account has used today against what its plan allows.\n\nRead from the meter, not from request logs: billing and quota decisions must not depend on\nsomething written for debugging.","examples":[{"datasets":["samples","tape"],"freshness":"up to the end of yesterday, UTC","freshness_cutoff":"2026-11-10T00:00:00Z","plan":"research","requests_per_minute":600,"resets_at":"2026-11-11T00:00:00Z","signed_urls_per_day":20000,"signed_urls_used_today":412}]},"UserOut":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"email":{"type":"string","title":"Email"},"plan":{"type":"string","title":"Plan","description":"Plan code. See `GET /v1/plans` for what each one covers."},"is_admin":{"type":"boolean","title":"Is Admin","description":"Back-office access. Not something a customer ever has."},"email_verified":{"type":"boolean","title":"Email Verified","description":"True once the address is proved: a verification link opened, a reset link used, or a Google sign-in with it."},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","email","plan","is_admin","email_verified","created_at"],"title":"UserOut","description":"An account. `plan` and `email` are plain strings on the way out, by the rule above.","examples":[{"created_at":"2026-11-08T09:12:00Z","email":"quant@example.com","email_verified":true,"id":"4a7f0b6e-0b4a-4d1e-9a3c-9f0f3f0f1a2b","is_admin":false,"plan":"research"}]},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyEmailRequest":{"properties":{"token":{"type":"string","title":"Token","description":"Everything after `#token=` in the link. Valid for 72 hours."}},"type":"object","required":["token"],"title":"VerifyEmailRequest","description":"The token from a verification link, which the web app reads out of the URL fragment.","examples":[{"token":"eyJ1aWQiOiI0YTdmMGI2ZSJ9.Zx3k1Q.s1gn4ture"}]}},"securitySchemes":{"ApiKeyBearer":{"type":"http","description":"An API key from `POST /v1/auth/register` or `POST /v1/keys`. Shown once at creation and stored only as a hash, so a lost key is replaced rather than recovered.","scheme":"bearer","bearerFormat":"pmd_<prefix>_<secret>"}}},"tags":[{"name":"catalog","description":"Markets, tokens, resolutions and the coverage ledger. **Public** - no key required, because a dataset nobody can inspect is a dataset nobody can cite."},{"name":"data","description":"Delivery of the tape as short-lived signed URLs, plus self-service usage and quota. Entitlement and quota are checked before anything is signed."},{"name":"auth","description":"Accounts and API keys. A key is shown once at creation and stored only as a hash."},{"name":"admin","description":"Back office: accounts and plans, including the manual academic grant. Requires an account with `is_admin`. A browsable UI lives at `/admin` when enabled."},{"name":"billing","description":"The price list and ordering. An order is placed, invoiced and approved by hand - nothing is charged here and no payment details are collected. A payment provider slots in behind the same flow later."},{"name":"health","description":"Liveness and readiness. `/ready` touches the database."}]}