1. Authentication
Send your key as a Bearer token:
Authorization: Bearer gw_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX- Keys look like
gw_live_followed by 43 URL-safe base64 characters. The full key is shown once when you create it; we store only its SHA-256 hash. Lose it, revoke it, create a new one. - A key acts as its account for GET requests only. Any other method under
/v1/answers405 method_not_allowed. Keys cannot change subscriptions, settings or billing. - A revoked key, an unknown key or a missing header answers
401(invalid_api_key/unauthorized). A key of an account that is no longer on Team answers403 plan_required.
2. Limits
| Limit | Value | When exceeded |
|---|---|---|
| Keys per account | 5 | 409 limit_reached on create |
| Requests per key per minute | 60 | 429 rate_limited (retry after the seconds in the message) |
| Requests per key per calendar month (UTC) | 100,000 | 429 quota_exceeded, error.details.resetAt = first of next month |
Every successful /v1 answer carries x-gliese-quota-remaining (requests left this month) and x-gliese-quota-reset (ISO instant). /v1 responses are Cache-Control: no-store.
3. Response envelope
Lists and single objects are wrapped the same way:
{ "data": [ ... ] | { ... }, "nextCursor": "128500" | null }nextCursor is opaque; pass it back as ?cursor= to fetch the next page. It is null on the last page and on single-object endpoints.
Errors:
{ "error": { "code": "quota_exceeded", "message": "monthly API quota of 100000 requests exhausted", "details": { "limit": 100000, "used": 100001, "resetAt": "2026-10-01T00:00:00.000Z" } } }4. Endpoints
GET /v1/events
Public change feed across all chains, newest first.
| Query | Meaning |
|---|---|
chain | Chain slug or id: ethereum, arbitrum, base, optimism, polygon, bnb (or 1, 42161, ...) |
address | Emitting contract, 0x + 40 hex |
severity | Minimum severity: critical, high, info |
key | Exact event key, e.g. proxy.upgraded |
category | upgrade, ownership, access, pause, multisig, timelock, governance, token_admin |
includeInit | true to include deployment-time (setup) events; default false |
limit | 1..200, default 50 |
cursor | From the previous page |
Item:
{
"id": 128501, "chainId": 1, "chainSlug": "ethereum",
"blockNumber": 25917480, "blockTime": "2026-09-06T09:44:12.000Z",
"txHash": "0x78e1...acd7", "logIndex": 7, "address": "0x8787...a4e2",
"eventKey": "proxy.upgraded", "category": "upgrade", "severity": "critical",
"args": { "implementation": "0x1234...abcd" }, "isInit": false,
"contract": { "name": "Pool", "protocol": "Aave", "kind": "proxy_transparent" }
}Synthetic slot.* events (silent changes found by the hourly poller) carry a derived txHash that does not exist on chain; their args hold field, previous, current.
GET /v1/contracts/:chain/:address
One contract: row, latest control snapshot, per-key counts, the 50 latest non-setup events and the control profile.
{
"data": {
"chain": { "id": 1, "slug": "ethereum" }, "address": "0x8787...a4e2",
"contract": { "id": 12, "kind": "proxy_transparent", "name": "Pool", "protocol": "Aave", "implementation": "0x...", "admin": "0x...", "owner": null, "verified": true, "source": "seed", "metadata": {}, "firstSeenAt": "...", "lastCheckedAt": "..." },
"latestSnapshot": { "implementation": "0x...", "admin": "0x...", "owner": null, "beacon": null, "adminIsContract": true, "ownerIsContract": null, "extra": { "safeThreshold": null, "minDelay": null, "blockNumber": 25917000 }, "takenAt": "..." },
"counts": [ { "eventKey": "proxy.upgraded", "total": 3, "real": 2, "init": 1 } ],
"recentEvents": [ ... ],
"controlProfile": { "label": "timelocked", "tone": "green", "reasons": ["admin 0x1234…abcd is a timelock with a 2d minimum delay"] }
},
"nextCursor": null
}controlProfile.label is one of immutable (Not upgradeable), single_key (Upgradeable by a single key), multisig (Multisig, no delay), timelocked (Multisig + 24h+ timelock), short_timelock (Timelock under 24h), unknown (Not profiled yet); tone is green, red, amber or grey. 404 contract_not_found when the address has neither a row nor events.
GET /v1/protocols/:slug
A protocol across chains by label or slug (aave, ether-fi): its contracts (each with controlProfile), a controlSummary and the 50 latest events.
{
"data": {
"protocol": "Aave", "slug": "aave",
"contracts": [ { "chainId": 1, "chainSlug": "ethereum", "address": "0x...", "name": "Pool", "protocol": "Aave", "kind": "proxy_transparent", "verified": true, "source": "seed", "events30d": 4, "critical30d": 1, "lastEventAt": "...", "lastEventKey": "proxy.upgraded", "controlProfile": { "label": "timelocked", "tone": "green", "reasons": ["..."] } } ],
"recentEvents": [ ... ],
"counts": { "contracts": 23, "chains": 4, "events30d": 40, "critical30d": 3 },
"controlSummary": { "total": 23, "singleKey": 3, "multisig": 8, "shortTimelock": 1, "timelocked": 10, "immutable": 0, "unknown": 1 }
},
"nextCursor": null
}GET /v1/me/subscriptions
The key owner's subscriptions.
{ "data": [ { "id": 77, "chainId": 1, "chainSlug": "ethereum", "address": "0x...", "source": "manual", "minSeverity": "high", "channels": { "telegram": true, "email": false, "webhook": false }, "createdAt": "...", "pausedAt": null, "contract": { "name": "Pool", "protocol": "Aave", "kind": "proxy_transparent" } } ], "nextCursor": null }GET /v1/me/notifications
The key owner's alert deliveries, newest first (limit 1..500, default 100; cursor from the previous page). At most the last 500 are meant to be read this way; use the events feed for history.
{ "data": [ { "id": 4711, "channel": "telegram", "status": "sent", "attempts": 1, "lastError": null, "createdAt": "...", "sentAt": "...", "event": { ...event item... } } ], "nextCursor": "4650" }5. Badges (no key needed)
GET /badge/:chain/:address.svg?theme=light|dark and GET /badge/protocol/:slug.svg return a 250x54 SVG with the control status, cached for five minutes. Embed:
<a href="https://gliese.io/contracts/ethereum/0x8787...a4e2"><img src="https://api.gliese.io/badge/ethereum/0x8787...a4e2.svg" alt="Control status on Gliese Watch" width="250" height="54"></a>6. curl examples
KEY=gw_live_...
# Critical changes on Base, two pages
curl -s -H "Authorization: Bearer $KEY" "https://api.gliese.io/v1/events?chain=base&severity=critical&limit=50" | jq '.nextCursor, (.data | length)'
curl -s -H "Authorization: Bearer $KEY" "https://api.gliese.io/v1/events?chain=base&severity=critical&limit=50&cursor=128450"
# One contract with its control profile
curl -s -H "Authorization: Bearer $KEY" https://api.gliese.io/v1/contracts/ethereum/0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2 | jq .data.controlProfile
# A protocol summary
curl -s -H "Authorization: Bearer $KEY" https://api.gliese.io/v1/protocols/aave | jq .data.controlSummary
# Your subscriptions and recent deliveries
curl -s -H "Authorization: Bearer $KEY" https://api.gliese.io/v1/me/subscriptions | jq '.data[] | {chainSlug, address, minSeverity}'
curl -s -i -H "Authorization: Bearer $KEY" "https://api.gliese.io/v1/me/notifications?limit=20" | grep -i x-gliese-quota7. Managing keys (web session, same origin as the app)
GET /me/api-keys->{ plan, items: [{ id, name, prefix, createdAt, lastUsedAt, usageThisMonth }], usage: { month, requests, limit, resetAt }, limits: { apiKeys, apiRequestsPerMinute, apiRequestsPerMonth } }POST /me/api-keys { name }->201 { id, name, prefix, key, createdAt, lastUsedAt, usageThisMonth }(keyappears only here);403 plan_requiredoutside Team,409 limit_reachedat 5 keysDELETE /me/api-keys/:id->{ revoked: true, id }
Questions and requests: [email protected].