Eyrium API
A read-only REST API for your own validators: current on-chain state, every alert event Eyrium detected, and per-epoch attestation rewards with ideal-reward comparison. Included on the free tier and Pro — no separate API subscription.
Scope note: this is not a chain explorer API. It returns data about validators on your account only.
Authentication
Create a key in Settings → API (sign in first). Keys look like eyk_… and are shown once. Send the key as a bearer token:
curl -H "Authorization: Bearer eyk_YOUR_KEY" https://api.eyrium.app/v1/validatorsLimits: 5 active keys per account, 60 requests per minute per key (HTTP 429 with Retry-After beyond that). Errors use a uniform envelope: {"error":{"code":"…","message":"…"}} with codes unauthorized, not_found, invalid_request, rate_limited.
GET /v1/validators
Every validator on your account with its latest known on-chain state. No parameters.
curl -H "Authorization: Bearer eyk_YOUR_KEY" https://api.eyrium.app/v1/validators
{
"validators": [
{
"id": "0b892948-9d0e-4a04-bd0e-3a59aa7f9c1a",
"pubkey": "0x86fd…549c",
"validator_index": 123456,
"label": "home-validator",
"created_at": "2026-07-01T09:00:00+00:00",
"state": {
"status": "active_ongoing",
"balance_gwei": 32004120345,
"effective_balance_gwei": 32000000000,
"fee_recipient": "0xabc…def",
"withdrawal_credentials": "0x01…",
"last_attestation_epoch": 371204,
"last_proposal_slot": 11877211,
"updated_at": "2026-07-22T10:04:11+00:00"
}
}
]
}state is null for a just-added validator that hasn't been polled yet.
GET /v1/events
Everything Eyrium detected for your validators — missed attestations and proposals, balance drops, status changes (slashing, exit, withdrawal), credential changes, proposed blocks, processed withdrawals. Newest first, cursor-paginated.
type— filter to one event type: balance_drop, block_proposed, missed_attestation, missed_proposal, state_change, status_change, withdrawal_processedvalidator_id— filter to one of your validators (UUID from /v1/validators)since— ISO 8601 timestamp — only events at or after this timelimit— page size, 1–100 (default 50)cursor— opaque cursor from the previous response's next_cursor
curl -H "Authorization: Bearer eyk_YOUR_KEY" \
"https://api.eyrium.app/v1/events?type=missed_attestation&limit=2"
{
"events": [
{
"id": "8f6f7f4e-…",
"validator_id": "0b892948-…",
"type": "missed_attestation",
"epoch": 371190,
"slot": null,
"payload": {
"head_reward": 0,
"target_reward": 4191,
"source_reward": 2257,
"ideal_head_reward": 3021,
"missed": ["head"],
"severity": "minor"
},
"created_at": "2026-07-22T08:11:32+00:00"
}
],
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0…"
}GET /v1/rewards
Per-epoch attestation rewards for your validators, including the ideal (maximum attainable) reward for the same duty — the delta is what a miss actually cost. Newest epoch first, cursor-paginated.
validator_id— filter to one of your validatorsfrom_epoch— inclusive lower epoch boundto_epoch— inclusive upper epoch boundlimit— page size, 1–1000 (default 500)cursor— opaque cursor from the previous response's next_cursorformat— json (default) or csv
curl -H "Authorization: Bearer eyk_YOUR_KEY" \
"https://api.eyrium.app/v1/rewards?from_epoch=371000&to_epoch=371010"
{
"rewards": [
{
"validator_id": "0b892948-…",
"epoch": 371010,
"head_reward": 3021,
"target_reward": 5814,
"source_reward": 3130,
"ideal_head_reward": 3021,
"ideal_target_reward": 5814,
"ideal_source_reward": 3130,
"effective_balance_gwei": 32000000000,
"recorded_at": "2026-07-22T10:04:11+00:00"
}
],
"next_cursor": null
}CSV export
format=csv streams your full reward history (respecting validator_id, from_epoch, and to_epoch) in one response — limit and cursor are ignored. Columns, in order: validator_id, epoch, head_reward, target_reward, source_reward, ideal_head_reward, ideal_target_reward, ideal_source_reward, effective_balance_gwei, recorded_at. Reward values are gwei. Exports past 499,500 rows end with a # TRUNCATED line — narrow the epoch range and request the remainder.
curl -H "Authorization: Bearer eyk_YOUR_KEY" \
-o rewards.csv "https://api.eyrium.app/v1/rewards?format=csv"GET /v1/effectiveness
Attestation effectiveness — the share of the maximum attainable reward your validators actually earned over the window — per validator plus an account rollup. The rollup is gwei-weighted (total actual ÷ total ideal), and missed_gwei is what the gap cost you.
window— 7d (default) or 30d, anchored to your newest recorded epoch
curl -H "Authorization: Bearer eyk_YOUR_KEY" \
"https://api.eyrium.app/v1/effectiveness?window=7d"
{
"window": "7d",
"validators": [
{
"validator_id": "0b892948-…",
"actual_gwei": "18803155",
"ideal_gwei": "18871420",
"effectiveness_pct": 99.64
}
],
"totals": {
"actual_gwei": "93417072",
"ideal_gwei": "94112830",
"missed_gwei": "695758",
"effectiveness_pct": 99.26
}
}Webhooks
Push instead of poll. When an alert fires for one of your validators, Eyrium POSTs a signed JSON body to an HTTPS endpoint you own — the same events that go out by email and Telegram. Webhooks are a Pro feature; add one in Settings → Alerts, up to 5 per account.
Adding a webhook sends a verification ping. Your endpoint has to answer 2xx within 10 seconds to be marked verified, and an unverified webhook receives nothing. If it was down at the time, use Send test once it's up. Rotating the secret unverifies the webhook until the next successful ping.
Headers
| Header | Value |
|---|---|
X-Eyrium-Signature | sha256= followed by the lowercase hex HMAC-SHA256 of the raw request body, keyed with this webhook's whsec_… secret. |
X-Eyrium-Event | Event type, e.g. missed_attestation. Same value as type in the body. |
X-Eyrium-Delivery | The event UUID. Stable across retries — dedupe on it. |
Content-Type | application/json |
User-Agent | Eyrium-Webhook/1 |
Payload
{
"version": 1,
"id": "8f6f7f4e-…",
"type": "missed_attestation",
"timestamp": "2026-07-26T09:14:03.221Z",
"epoch": "412903",
"slot": null,
"validator": {
"id": "0b892948-…",
"index": "123456",
"pubkey": "0x86fd…549c",
"label": "home-validator"
},
"data": {
"head_reward": 0,
"target_reward": 4191,
"source_reward": 2257,
"ideal_head_reward": 3021,
"missed": ["head"],
"severity": "minor"
},
"url": "https://eyrium.app/dashboard/validators/0b892948-…"
}version— 1 today. New fields are added without a bump; a structural change bumps it.id— Event UUID — the same value as X-Eyrium-Delivery and as id in /v1/events.type— Event type, or ping for a verification ping. Same set as /v1/events.timestamp— ISO 8601 UTC time Eyrium recorded the event. Inside the signature — use it for replay checks.epoch, slot— Strings, not JSON numbers: they can exceed the JS safe-integer range and would silently lose precision. null when the event isn't tied to one.validator— id, index (also a string), pubkey and label of the affected validator. null on a ping.data— The raw detector payload, passed through untouched — the same object /v1/events returns as payload. Fields vary by event type.url— Dashboard deep link for the validator. null on a ping.
On missed_attestation, data.missed lists the reward components that earned nothing — any subset of head, target, source — and data.severity is minor when only the head vote was missed and major when target or source was. Head misses are usually slow block propagation or a reorg; target and source are the expensive components. Route on severity if you want to page on one and log the other.
Both fields are also present on payload in /v1/events, on events recorded from 26 July 2026 onward. Earlier events carry the raw *_reward components without the derived tag.
The verification ping uses the same envelope:
{
"version": 1,
"id": "00000000-0000-0000-0000-000000000000",
"type": "ping",
"timestamp": "2026-07-26T09:00:00.000Z",
"epoch": null,
"slot": null,
"validator": null,
"data": { "message": "Eyrium webhook verification" },
"url": null
}Verifying the signature
Compute HMAC-SHA256 over the raw request body bytes with your whsec_… secret and compare it to X-Eyrium-Signature in constant time. Parsing the JSON and re-serializing it will not match — a single byte of whitespace changes the digest, so capture the body before your framework touches it.
Node
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(header ?? "");
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Python
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(header or "", expected)Replay and duplicates
There is no separate timestamp header — the body's timestamp is covered by the signature, so it can't be tampered with. After the signature checks out, reject anything older than 5 minutes. A retry carries the same X-Eyrium-Delivery value as the original, so key your idempotency on that.
Delivery and retries
2xx— Success. The response body is ignored.429, 5xx, timeout, network error— Retried up to 3 times with backoff. Requests time out after 10 seconds.other 4xx— Not retried — recorded as a failed delivery, with the first 200 characters of your response body kept as the error.410 Gone— Disables the webhook immediately. Use it when an endpoint is retired for good.
Failures are counted consecutively and the counter resets on the first success. After 20 failed attempts in a row the webhook is disabled automatically — fix the endpoint, then send a test from Settings to re-enable it.
Destination rules
Destinations must be https:// and publicly routable. Loopback, private and link-local addresses (127.0.0.0/8, 10/8, 172.16/12, 192.168/16, 169.254/16, ::1, fc00::/7), the hostnames localhost, .local, .internal and .localhost, and URLs with embedded credentials are all rejected. The check runs when you add the webhook and again on every send. To reach a homelab box, expose it through a tunnel or a public reverse proxy.
Coming later
Per-webhook event routing (send only the types you care about to each endpoint) is on the roadmap. If you need an endpoint that isn't here, say so: [email protected].
Free for your first validator.
Sign up, add a validator by pubkey or index, create a key in Settings, and the API works. Pro ($7/mo) lifts you to 200 validators.
Get started →