Gains Deposits API

Deposit volume and transaction count by payment method, scraped hourly from the funkit Hex dashboard.

Authentication

Every endpoint needs the same header. There is no second scheme and no unauthenticated data path.

Authorization: Bearer $API_TOKEN

No header, a wrong token, or a Basic header all return 401 with WWW-Authenticate: Bearer. Ask makushi for the token — it is not on this page.

Figures are cumulative. Every scrape returns all-time totals over the dashboard's default 2-year window. There is no per-period number in the source. A weekly or daily figure is the difference between two snapshots — that is what /delta is for. Never read a /latest or /history row as a period total.

Endpoints

GET/health

Refresh state, last error, cookie expiry. 200 healthy · 503 starting (no successful refresh yet) or degraded (last refresh failed, or the cookie is past expiry). Same body either way.

{
  "status": "ok",
  "lastSuccessAt": "2026-09-23T17:07:41.009Z",
  "lastError": null,
  "sessionCookieOk": true,
  "cookieExpiresAt": "2026-10-07T17:07:12.274Z",
  "daysUntilCookieExpiry": 14,
  "cookieExpirySource": "rotated",
  "snapshotCount": 6,
  "scrapeInFlight": false,
  "refreshCron": "0 * * * *",
  "nextRefreshAt": "2026-09-23T18:00:00.000Z"
}
GET/latest

Most recent snapshot. 503 if no scrape has succeeded yet.

{
  "basis": "cumulative-all-time",
  "capturedAt": "2026-09-23T17:07:41.009Z",
  "updatedDate": "2026-09-23T17:07:22.781Z",
  "rows": [
    { "category": "Fiat",            "txCount": 122, "volumeUsd": 6109 },
    { "category": "Other",           "txCount": 1,   "volumeUsd": 0 },
    { "category": "Transfer Crypto", "txCount": 318, "volumeUsd": 75033 },
    { "category": "Wallet",          "txCount": 37,  "volumeUsd": 2739 }
  ]
}

capturedAt is when this service stored the snapshot; updatedDate is when Hex executed the cell. Always within 30 minutes of each other, or the scrape is rejected as stale.

GET/history
QueryRequiredMeaning
fromnoISO timestamp, inclusive. Defaults to the epoch
tonoISO timestamp, inclusive. Defaults to now

Snapshots whose capturedAt falls in the range, oldest first. Still cumulative totals — one row set per refresh.

GET /history?from=2026-09-01T00:00:00Z&to=2026-09-23T00:00:00Z

{
  "basis": "cumulative-all-time",
  "from": "2026-09-01T00:00:00.000Z",
  "to": "2026-09-23T00:00:00.000Z",
  "count": 6,
  "snapshots": [ { "capturedAt": "…", "updatedDate": "…", "rows": [ … ] } ]
}

400 if a timestamp is unparseable or from is later than to.

GET/delta
QueryRequiredMeaning
fromyesISO timestamp — start of the period
toyesISO timestamp — end of the period

Per-category difference between the snapshots nearest each bound. This is the endpoint that answers "how much was deposited in crypto vs fiat this week".

GET /delta?from=2026-09-16T00:00:00Z&to=2026-09-23T00:00:00Z

{
  "basis": "delta",
  "requested": { "from": "2026-09-16T00:00:00.000Z", "to": "2026-09-23T00:00:00.000Z" },
  "from": { "capturedAt": "2026-09-16T00:00:12.041Z", "updatedDate": "…" },
  "to":   { "capturedAt": "2026-09-23T00:00:09.882Z", "updatedDate": "…" },
  "spanHours": 168,
  "rows": [
    { "category": "Fiat",            "txCount": 80,  "volumeUsd": 1400.5 },
    { "category": "Transfer Crypto", "txCount": 405, "volumeUsd": 41250.75 }
  ],
  "totals": { "txCount": 497, "volumeUsd": 42951.25 }
}

Check the returned from/to before quoting a number — "nearest" can be hours off what you asked for if a refresh failed. A category present in only one endpoint counts as 0 in the other.

400 a bound is missing or the range is reversed · 422 both bounds resolve to the same snapshot · 503 fewer than two snapshots exist.

POST/cookie

Replace the Hex session cookie at runtime — no restart. The cookie has a fixed 14-day TTL that traffic does not extend, so this gets used roughly fortnightly.

FieldRequiredMeaning
cookieyesThe connect.sid value, exactly as DevTools shows it
issuedAtnoISO timestamp of when it was issued. Defaults to now — correct if you just grabbed it
refreshnoDefault true: scrape immediately to prove the new cookie works
POST /cookie
Content-Type: application/json

{ "cookie": "s%3A…", "issuedAt": "2026-09-23T17:00:00Z", "refresh": true }

{
  "cookie": {
    "fingerprint": "8bcb330f0881",
    "expiresAt": "2026-10-07T17:00:00.000Z",
    "daysUntilExpiry": 14,
    "source": "rotated-with-issued-at"
  },
  "refreshTriggered": true,
  "refreshSkipped": null
}

The value is never logged and never returned — only a fingerprint. 400 if it is not a signed connect.sid value, the body is over 8 KB, or issuedAt is in the future.

Notes

RefreshHourly. Each run drives a real headless Chrome session and takes 90–150 s. One at a time — a tick that fires during a run is skipped, not queued.
FailureNo retries: a failed scrape waits for the next tick. The last good snapshot keeps serving; only /health changes.
StalenessCell output older than 30 minutes is rejected rather than stored — Hex serving a cached run is a failure, not a result.
ErrorsAlways {"error": "…"} with a plain-language message.