docs/API.md, served as-is · machine-readable: /openapi.json (the developer API only) · the interactive /docs and /redoc are closed in production by design
Single-file clients, standard library only: Python · JavaScript. Developer API keys are minted in your account on plans that include them (pricing).
# DeepGyre API
Base URL: `https://deepgyre.com`
This reference is what `/api-docs` shows. The interactive explorers (`/docs`,
`/redoc`) and the full schema are closed in production by design: they would
publish every internal route. `/openapi.json` is an OpenAPI 3 document of the
developer API (`/api/v1/data/*`) only. What a client should call is listed here.
---
## First call in under a minute
Several routes need no account:
```bash
curl https://deepgyre.com/api/v1/health
```
It answers `status`, the list of supported `chains` and whether `maintenance`
is on. The full diagnostic picture is the operator's, not public.
A real query, no auth (`limit` has a floor of 10):
```bash
curl "https://deepgyre.com/api/v1/market/defi/revenue?limit=10"
```
---
## Authentication
There are two doors. Programs use the first.
### 1. Developer API keys: `/api/v1/data/*`
Plans that include the developer API (see `/pricing`) mint keys in the app:
Account → Developer API, or `POST /api/v1/keys` from a signed-in session. The
raw key is shown ONCE and stored only as a hash; `DELETE /api/v1/keys/{id}`
revokes it at once.
```bash
curl -H "X-API-Key: $DEEPGYRE_KEY" \
"https://deepgyre.com/api/v1/data/smart-money/leaderboard?limit=20"
```
Every `/data` response carries `X-API-Calls-Used` and `X-API-Calls-Limit`, so
the meter is visible without a second request. A missing or revoked key answers
`401`; a key past its monthly quota answers `429`. The plan is re-read on every
call: a key whose owner leaves a plan with the developer API stops working.
| Route | Returns |
|---|---|
| `GET /api/v1/data/smart-money/leaderboard?limit=10..200` | wallets ranked by measured earliness, with the basis line |
| `GET /api/v1/data/token-map/{contract}?limit=5..60` | the largest net buyers and sellers of an Ethereum token over the index window |
| `GET /api/v1/data/entities` | the curated entity book: slug, name, type, verified, address count, tracked value (`null` = never aggregated, not zero) |
| `GET /api/v1/data/label/{address}` | the platform's label for one address; unknown is `null`, never a guess |
| `GET /api/v1/keys/usage` | (session) your entitlement and this month's meter |
### 2. The session cookie: what the app itself uses
Signing in (`POST /api/v1/auth/login`) sets an `httpOnly` session cookie for
seven days; state-changing requests also need the `X-CSRF-Token` header the
app reads from the `bs_csrf` cookie. It is built for the browser app, not for
scripts: a program should use an API key.
---
## Plan gating
Subscription-gated routes answer **402** when your plan does not include them,
with a body naming what is needed:
```json
{"detail": {"error": "upgrade_required", "tab": "whale", "current_plan": "free"}}
```
Which tabs each plan opens is set by the operator and can change, so it is not
restated here: `GET /api/v1/auth/entitlements` returns your account's effective
list. A new account opens every tab for the length of its free trial.
---
## Rate limits
| Scope | Limit | On exceed |
|---|---|---|
| Per IP, all routes | 240 / minute | `429` |
| Heavy aggregation routes | 20 / 60s | `429` + `Retry-After` |
| Contract scans | 10 / minute | `429` |
Honour `Retry-After`. Responses are not penalised for retrying after it.
---
## Data freshness
Expensive aggregates are served stale-while-revalidate: you get the cached
answer immediately and a refresh runs behind it. Those responses carry:
```json
{"data_age_seconds": 139, "stale": true}
```
`stale: true` is not an error — it means the value is past its TTL and a
refresh is in flight. Prefer it to waiting: a cold recompute of
`/macro/overview` takes ~76s.
---
## Endpoints worth starting with
### Market
| Route | Auth | Notes |
|---|---|---|
| `GET /api/v1/health` | — | `status`, `chains`, `maintenance` |
| `GET /api/v1/market/defi/revenue?limit=100` | — | fees, revenue, take rate |
| `GET /api/v1/market/defi/revenue/{slug}` | — | one protocol |
| `GET /api/v1/market/onchain/metrics` | — | SOPR, HODL Waves, Realized Cap, MVRV, NUPL |
| `GET /api/v1/market/regime` | plan `regime` | bull/bear score + 7 signals + altseason |
| `GET /api/v1/market/cycle` | plan `regime` | cycle position 0-100. **Inverted vs `/regime`**: 0 = cycle bottom, 100 = cycle top |
| `GET /api/v1/market/cycle/backtest` | plan `regime` | walk-forward replay; in-sample, caveats in the payload; `computing: true` on a cold cache |
| `GET /api/v1/market/cycle/history?days=365` | plan `regime` | cycle-score timeline |
| `GET /api/v1/market/supply-in-loss` | plan `supplyloss` | measured series (2022→) + reconstructed (2010→) with its λ band; `computing: true` on a cold cache |
| `GET /api/v1/market/smart-money` | plan `whale` | cohort accumulation |
### Wallet & entity
| Route | Auth | Notes |
|---|---|---|
| `GET /api/v1/wallet/profile/{chain}/{address}` | — | net worth, counterparties, threat |
| `POST /api/v1/wallet/flow` | — | flow graph; body `{chain, address, limit}` |
| `GET /api/v1/wallet/evm-chains?address=0x…` | session | which chain an address is active on |
| `GET /api/v1/intelligence/label/{address}` | — | entity label with provenance |
| `GET /api/v1/intelligence/entity/links/{address}` | — | observed counterparties |
### Alerts
| Route | Auth | Notes |
|---|---|---|
| `GET /api/v1/intelligence/alerts/rules` | session | your rules |
| `POST /api/v1/intelligence/alerts/rules` | session | create one |
---
## Reading a response honestly
The API distinguishes "we could not compute this" from "the value is zero", and
you should propagate that distinction rather than defaulting to 0.
```json
{"metric": "mvrv", "available": false,
"reason": "Realized Cap is partial (…) — reporting that as MVRV would be a
wrong number under a familiar name",
"blocked_by": "realized_cap_partial"}
```
Fields you will meet:
| Field | Meaning |
|---|---|
| `available: false` | not computable; `reason` says why |
| `partial` | computed over an incomplete window; treat as a bound |
| `stale` / `data_age_seconds` | served from cache, age in seconds |
| `source` / `method` / `confidence` | how a label or figure was derived |
| `excluded_transfers` / `excluded_reason` | what was filtered from a total, and why |
Labels carry provenance. A curated label has `confidence: 1.0`; one inferred
from the local index carries `inferred: true`, its `method`, and a confidence
below 1. An address we cannot identify returns `found: false` — never a guess.
---
## Errors
| Status | Meaning |
|---|---|
| `400` | malformed input (the body names the field) |
| `401` | missing or expired session, or a missing / revoked API key |
| `402` | your plan does not include this route |
| `403` | authenticated but not permitted (admin routes) |
| `422` | a required query parameter is missing or malformed |
| `429` | rate limited (honour `Retry-After`), or an API key past its monthly quota |
| `5xx` | server error; `request_id` in the body is worth quoting |
---
## SDKs
Single-file, standard library only, not on a package registry yet:
* Python: `https://deepgyre.com/sdk/deepgyre.py`
* JavaScript (Node 18+ and browsers): `https://deepgyre.com/sdk/deepgyre.js`
```python
from deepgyre import DeepGyre
dg = DeepGyre(api_key="…") # base URL defaults to deepgyre.com
print(dg.health()["status"])
print(dg.data_smart_money(limit=20)["rows"][0])
```
```javascript
import { DeepGyre } from './deepgyre.js';
const dg = new DeepGyre(undefined, { apiKey: '…' });
console.log((await dg.health()).status);
console.log(await dg.dataLabel('0x28c6c06298d514db089934071355e5743bf21d60'));
```
Both pass `available: false` through untouched and retry a `429` after
`Retry-After` instead of hammering.