RAILGUN liveness history API
The liveness API answers "what does Anon's monitor see right now?". This endpoint answers "what has it seen over the last two weeks?". It returns, for every service and chain the live report lists, one character per 5-minute bucket for up to 14 days, and an uptime figure over the last 24 hours, 7 days and 14 days. It is enough to draw a status-page bar chart. Anyone can read it. There is no API key and no account.
GET https://api.anon.inc/api/v1/railgun/liveness/history
This is Anon's own monitor's view, from one place. It is not official Railgun status, and it is not a measurement of Railgun itself: every character is a status Anon's monitor published. Limits lists what it cannot tell you.
Beta: this API is in beta, with the same promise as the live endpoint. Fields, values and the shape of the response may change without notice. Write your client to ignore unknown fields and values. See Beta status.
| Property | Value |
|---|---|
| Base URL | https://api.anon.inc |
| Authentication | None |
| CORS | Any origin, no credentials (GET, HEAD, OPTIONS) |
| Status | Beta: may change without notice (Beta status) |
| Format | JSON, schemaVersion 1 (JSON Schema) |
| Resolution | 5-minute buckets, up to 14 days |
| Cache | 60 seconds in browsers, 120 seconds in shared caches |
Quick start
curl -i https://api.anon.inc/api/v1/railgun/liveness/history
The response carries the history and its cache headers:
HTTP/2 200
content-type: application/json; charset=utf-8
cache-control: public, max-age=60, s-maxage=120, stale-while-revalidate=60, stale-if-error=300
etag: "5b3f0c9a1d7e4426"
access-control-allow-origin: *
access-control-expose-headers: ETag
Send the ETag back with If-None-Match to ask whether anything changed. If not, the answer is 304 Not Modified with no body:
curl -i https://api.anon.inc/api/v1/railgun/liveness/history \
-H 'If-None-Match: "5b3f0c9a1d7e4426"'
There are no query parameters. Every request returns all of the stored history, a few kilobytes after compression, because the strings are mostly one repeated character.
A small TypeScript client. It reads one row, squeezes its buckets into a fixed number of bars, and formats the uptime. A null uptime is shown as "no data", never as 100%, and a character it does not know counts as no data:
type Uptime = Record<"24h" | "7d" | "14d", number | null>;
interface HistoryRow {
service: string;
chainId: number | null; // null for waku
buckets: string; // one character per bucket, oldest first
uptime: Uptime;
}
interface History {
schemaVersion: number;
generatedAt: string;
bucketSeconds: number;
retentionDays: number;
from: string;
to: string;
rows: HistoryRow[];
}
const res = await fetch("https://api.anon.inc/api/v1/railgun/liveness/history", {
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(8000),
});
if (!res.ok) throw new Error(`liveness history: HTTP ${res.status}`);
const report: History = await res.json();
if (report.schemaVersion !== 1) throw new Error("liveness history: unknown schemaVersion");
// Worst known state wins. Anything else in a bar is only shown if nothing known is.
const SEVERITY: Record<string, number> = { c: 0, k: 1, d: 2, u: 3 };
function barState(chars: string): string {
let worst = "";
for (const ch of chars) {
if (ch in SEVERITY && (worst === "" || SEVERITY[ch] > SEVERITY[worst])) worst = ch;
}
if (worst) return worst;
if (chars.includes("?")) return "?"; // unknown: no evidence
if (chars.includes(".")) return "."; // unmonitored
return "-"; // no data, or a character this client does not know
}
// Split the buckets into `bars` groups of nearly equal size, oldest first. Fewer
// buckets than bars gives one bar per bucket.
function downsample(buckets: string, bars: number): string[] {
const count = Math.min(bars, buckets.length);
const out: string[] = [];
for (let i = 0; i < count; i++) {
const start = Math.floor((i * buckets.length) / count);
const end = Math.floor(((i + 1) * buckets.length) / count);
out.push(barState(buckets.slice(start, end)));
}
return out;
}
// The server rounds to 4 decimals, so this is exact. null means "not enough data",
// which is not the same as 0% or 100%.
function formatUptime(value: number | null | undefined): string {
if (typeof value !== "number" || !(value >= 0 && value <= 1)) return "no data";
return `${(Math.round(value * 10000) / 100).toFixed(2)}%`;
}
for (const row of report.rows) {
const label = `${row.service} ${row.chainId ?? "-"}`;
console.log(label, downsample(row.buckets, 90).join(""), formatUptime(row.uptime["14d"]));
}
Match rows by service and chainId, and ignore rows you do not use. Do not rely on their order.
Endpoint
GET https://api.anon.inc/api/v1/railgun/liveness/history
- No authentication and no parameters. Any query string is ignored.
HEADreturns the headers only.OPTIONSanswers CORS preflights.- The body is
application/json; charset=utf-8. - One response covers every service on every chain. Fetch it once and filter.
- The rows are the same ones, in the same order, as the live report at
/api/v1/railgun/liveness: 21 today. See Services and chains for what each service is.
Example response
A trimmed history with five rows, with illustrative values. It covers only two hours, which is what the endpoint returns just after it starts recording. A real response lists every row, and its buckets strings grow to 4033 characters (How long the history is).
{
"schemaVersion": 1,
"generatedAt": "2026-10-06T14:03:11.402Z",
"bucketSeconds": 300,
"retentionDays": 14,
"from": "2026-10-06T12:00:00Z",
"to": "2026-10-06T14:00:00Z",
"rows": [
{
"service": "ppoi",
"chainId": 1,
"buckets": "cccccccccccccccckcccccccc",
"uptime": { "24h": 0.96, "7d": 0.96, "14d": 0.96 }
},
{
"service": "subsquid",
"chainId": 137,
"buckets": "cccccckkkkdddcccccccccccc",
"uptime": { "24h": 0.72, "7d": 0.72, "14d": 0.72 }
},
{
"service": "subsquid",
"chainId": 11155111,
"buckets": ".........................",
"uptime": { "24h": null, "7d": null, "14d": null }
},
{
"service": "broadcasters",
"chainId": 137,
"buckets": "cccccccccccccuccccccccccc",
"uptime": { "24h": 0.96, "7d": 0.96, "14d": 0.96 }
},
{
"service": "waku",
"chainId": null,
"buckets": "cccccc------ccccccccccccc",
"uptime": { "24h": 1, "7d": 1, "14d": 1 }
}
]
}
fromis2026-10-06T12:00:00Zandtois2026-10-06T14:00:00Z: 24 intervals, so everybucketsstring has 25 characters, one per bucket, oldest first.tois the start of the last bucket, which was still filling atgeneratedAt(14:03).- The
ppoirow has onekbucket: the service wascatching-upfor at least a minute of that 5-minute bucket, and nothing worse lasted that long. Its uptime is 24 of 25 known buckets, 0.96. - The
subsquidrow on Polygon (137) has fourkbuckets and then threedbuckets in a row, then recovers. 18 of 25 buckets arec, so its uptime is 0.72. - The
subsquidrow on Sepolia is.throughout: not monitored. Fewer than 12 known buckets, so every uptime isnull. - The
broadcastersrow has oneubucket: the service wasunavailablefor at least a minute of that 5-minute bucket. - The
wakurow has six-buckets: the monitor was not running or recorded nothing. They are left out of the uptime, which is 1 over the 19 known buckets. - The three uptime values are equal here because the history is only two hours long. Each window is clipped to the data that exists.
chainIdisnullforwaku, the one shared row.
Fields
History
| Field | Type | Meaning |
|---|---|---|
schemaVersion |
integer | 1 today. It may change while the API is in beta. |
generatedAt |
timestamp | When the stored history was last written, not when it was served. It moves forward about once a minute. |
bucketSeconds |
integer | The length of a bucket. 300 today. |
retentionDays |
integer | The most history kept. 14 today. |
from |
timestamp | The start of buckets[0], on a 300-second boundary. |
to |
timestamp | The start of the last bucket, the one still filling. |
rows |
array | One object per service and scope, in the same order as the live report. |
from and to are UTC with a literal Z and no fractional seconds: YYYY-MM-DDTHH:mm:ssZ. generatedAt has millisecond precision, like the live report.
The number of characters in every buckets string is (to − from) / bucketSeconds + 1. It is the same in every row.
Row
| Field | Type | Meaning |
|---|---|---|
service |
string | ppoi, indexer, subsquid, broadcasters or waku today. More may appear. Same meaning as in the live report. |
chainId |
integer or null |
Positive chain ID. null for waku today, which is one shared check. |
buckets |
string | One character per bucket from from to to inclusive, oldest first. |
uptime |
object | 24h, 7d and 14d, each a number from 0 to 1 or null. See Uptime. |
Sepolia (11155111) is a test network. Keep it out of any mainnet figure you compute, as with the live report.
Characters
| Character | Status published | Meaning |
|---|---|---|
c |
current |
The service answered and was caught up. |
k |
catching-up |
It answered but was behind. This is not an outage. |
d |
degraded |
It was seriously impaired. |
u |
unavailable |
Anon's monitor got no usable answer on three probes in a row. |
? |
unknown |
No usable evidence in that bucket. |
. |
unmonitored |
Anon does not monitor this check. |
- |
none | No data: the monitor was not running, or recorded no sample for that bucket. |
The statuses mean what they mean in the live report: see Status. This set may grow. Treat a character you do not know as - for that bucket only, and keep the rest of the row.
Draw ?, . and - in three different ways that are all clearly not green. They mean three different things: no evidence, not covered and not recorded.
How a bucket and the uptime are computed
A bucket
The monitor publishes a status for every row about every 30 seconds. The history keeps those published statuses, and a bucket's character comes from the ones published for that row during its 5 minutes:
- Take the known statuses:
current,catching-up,degradedandunavailable, by severityuoverdoverkoverc. The character is the worst status that lasted at least a minute in total during those 5 minutes. Time spent in a worse status counts toward it: 30 seconds ofdegradedand 40 seconds ofunavailablemake ad. Shorter blips, such as a restart, don't count, and the bucket showscinstead. - If there is none but some are
unknown, the character is?. - If there are only
unmonitoredstatuses, the character is.. - If there were no samples at all, the character is
-.
So a u means the service was unavailable for at least a minute of that bucket, and a c means nothing worse than current lasted a minute. A bucket is the worst status that lasted, not an average. The last bucket is still filling, so its character can change until it ends.
Uptime
For each window, uptime is:
c / (c + k + d + u)
counted over the buckets in the last 24 hours, 7 days or 14 days, clipped to the history that exists. The result is rounded to 4 decimals.
- Buckets with
?,.or-are left out of both sides of the fraction. They are neither passes nor failures. kcounts against uptime, as it does fordandu. A service that is catching up is not counted as up.- The value is
nullwhen a window has fewer than 12 known buckets: less than one hour of evidence. A fully unmonitored row isnull. nullis not0and not1. Show it as "no data".- A value of exactly
1means no known bucket in that window was anything butc. One non-cbucket in 14 days already gives at most0.9998.
You can compute your own window or your own mix of services from buckets. A figure of yours can differ slightly from the server's at the edges of a window.
How long the history is
from is the first bucket the monitor ever recorded, or the start of the 14-day window if that is later. The history only exists from the day the feature shipped, and there is no backfill. So the strings start at one character and grow by one every 5 minutes up to 4033 characters, which takes the first 14 days. After that the window slides: the oldest bucket drops off as a new one starts.
Until then, 24h, 7d and 14d can describe the same short period, and 7d and 14d can be null on a young history. Do not label a figure "14 days" without saying how much data stands behind it. Compare from with to for that.
Limits
- One vantage point. It is Anon's monitor, running from Anon's infrastructure, probing the services Anon is configured to use. Your view of those services can differ. A bucket does not show what your wallet or your network would have seen.
- It is what the monitor published, not a measurement of Railgun. A
ubucket says the monitor got no usable answer from the service it was configured to read, three times in a row. It does not prove the service was down for anyone else. It is not a "safe to transact" signal either: see What it does not tell you. - History starts at launch. Nothing older than the first recorded bucket exists, and there is no backfill. A young history is short, not perfect.
- No data is a gap in monitoring. A
-bucket is not an outage and not a pass. It means the monitor was not running or recorded nothing for those 5 minutes, so show it as a gap. Uptime leaves it out. - Unknown is not an outage either.
?means the monitor had no evidence. Uptime leaves it out. - Only 14 days. Older buckets are gone. If you need a longer record, store the response yourself.
- Coarse. A 5-minute bucket hides anything shorter. A problem that lasted under a minute in total does not show at all, and one that lasted a minute colors the whole bucket. Uptime inherits both.
- Beta. It may change without notice, like the live endpoint: see Beta status.
Freshness and caching
The history changes slowly: the newest bucket can change as samples arrive, and once the history is 14 days long the window moves forward one bucket every 5 minutes. The response carries:
Cache-Control: public, max-age=60, s-maxage=120, stale-while-revalidate=60, stale-if-error=300
| Directive | Effect |
|---|---|
max-age=60 |
A browser reuses its copy for 60 seconds. |
s-maxage=120 |
A shared cache, such as the CDN, reuses its copy for 2 minutes. |
stale-while-revalidate=60 |
A cache may serve a copy up to a minute past that while it fetches a fresh one. |
stale-if-error=300 |
If the origin errors, a cache may keep serving its copy for up to 5 minutes. |
How old can it be? generatedAt says when the stored history was last written. On top of that, the edge copy can be up to a few minutes old by the time it reaches you: up to 2 minutes of s-maxage plus up to a minute of stale-while-revalidate, and up to a minute more in a browser. Do not use this endpoint to decide whether a service is up right now. Use the live endpoint for that, and read its expiresAt.
Poll no faster than once a minute. Faster polling only returns cached copies. A page that shows a chart can fetch once when it opens and again when it is visible after a few minutes.
ETag and 304. The ETag is a quoted hash of the body. The monitor rewrites the history about once a minute, and generatedAt is part of the body, so generatedAt and the ETag both change about once a minute even when no bucket changed. Expect a 304 mostly when you revalidate within the same minute. If-None-Match is how an unchanged history costs a 304: it carries the ETag, Cache-Control and CORS headers and no body. The response sends no Vary: Origin. The CDN keeps a separate copy for each Origin request header value, and ignores the query string.
HTTP status codes and errors
| Status | When | Notes |
|---|---|---|
200 |
The history. | A 200 means the history is readable, not that the services are healthy. |
304 |
If-None-Match matched. |
No body. |
204 |
An OPTIONS preflight. |
See CORS. |
405 |
Any other method. | Allow: GET, HEAD, OPTIONS. |
503 |
The history is not available. | Sent until the first history is written, or when the stored history can't be read for more than five minutes. Carries Retry-After: 30 and Cache-Control: no-store. Retry with backoff, and keep showing the last good history you have. |
404 |
Any other path. | Answered by the gateway. |
429 |
You are above the rate limit in Fair use. | Comes from the CDN (Cloudflare error 1015) rather than the API. Lasts about 60 seconds, with Retry-After of about 60. |
Branch on the HTTP status code, never on the message text. An error from the API itself is {"error": "<message>"}. The gateway's 404 and a CDN 429 use other bodies: the 429 is a Cloudflare text/plain page.
In a browser, a 429 is not readable. The CDN's 429 page carries no Access-Control-Allow-Origin, so the browser blocks it and fetch rejects with a TypeError, the same as for any network or CORS failure. Your code never sees the status or Retry-After. Treat any network failure like a 429 or 503: back off and keep showing the last good history, marked with its generatedAt.
CORS
The history is public and has no per-caller content, so every response allows every origin:
| Header | Value |
|---|---|
Access-Control-Allow-Origin |
* |
Access-Control-Expose-Headers |
ETag |
There is no Access-Control-Allow-Credentials. A plain fetch with an Accept header is a simple request and needs no preflight. A script that sets If-None-Match itself does, and the endpoint answers its own OPTIONS preflight with 204. The allowed methods and headers are the same as on the live endpoint.
Fair use
The same rules as the live endpoint apply: there is no key and no SLA, respect Cache-Control and use ETag, send a descriptive User-Agent from servers, and expect 429 above about 120 requests per minute per IP on the route. Back off on 429 and on 5xx.
In addition, because this data moves slowly, poll no faster than once a minute, and prefer fetching when a user opens a screen that shows it. If you serve many users, fetch it from your own server and cache it there.
Beta status
This API is in beta, with the same promise as the live endpoint and no stronger.
- It may change without notice. Fields, characters, windows and the shape of the response may be added, renamed, retyped or removed, with no notice period and no deprecation or sunset commitment. The retention, the bucket length and the uptime windows are today's values.
schemaVersionmay change. If it is not a value you know, treat the history as unreadable until you have updated. The JSON Schema is permissive and describes today's response. It is not a promise.- Changes are recorded on a best-effort basis in the changelog below.
How to write a client that survives that:
- Ignore unknown fields. At every level: the history, each row and
uptime. - Do not rely on closed sets. Treat a bucket character you do not know as
-for that bucket only. Ignore rows whoseserviceorchainIdyou do not use. Ignoreuptimekeys you do not know. - Fail one row, not the whole history. One row you cannot read, for example a missing
uptimeor abucketsstring of the wrong length, should not blank the other rows. - Do not hard-code the length. Use
from,toandbucketSeconds.
Changelog
2026-10-07
Added. GET /api/v1/railgun/liveness/history returns up to 14 days of 5-minute buckets and 24-hour, 7-day and 14-day uptime for every row of the live report. schemaVersion 1, in beta. History is recorded from 2026-10-07 01:55 UTC, with no backfill.