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/validators

Limits: 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.

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.

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.

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

HeaderValue
X-Eyrium-Signaturesha256= followed by the lowercase hex HMAC-SHA256 of the raw request body, keyed with this webhook's whsec_… secret.
X-Eyrium-EventEvent type, e.g. missed_attestation. Same value as type in the body.
X-Eyrium-DeliveryThe event UUID. Stable across retries — dedupe on it.
Content-Typeapplication/json
User-AgentEyrium-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-…"
}

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

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 →