MeteraDocs

Build

Webhooks

Register once, get notified when an address changes — every notification re-verified the same way a direct call would be, never trusted from the trigger alone.

Every other endpoint on this API is pull: you ask, Metera answers. POST /v1/watch is the one push surface — you register an address and a condition, and Metera calls your webhookUrl when it fires. This is a different feature from Agents (scheduled collection on an interval you set): a watch fires on a real detected change, not on a clock.

The trigger never is the fact

What notices a change (polling today, an interval read against a single node) is never trusted as the thing delivered to you. It only wakes a real re-verification — the exact same dual-RPC call a direct get_verified_data would run, with its own traceId and its own billing. A false positive from the trigger costs one harmless re-check that finds nothing changed and delivers nothing. A false negative just means the next poll catches it. Either way, what reaches your webhook was independently re-verified, not forwarded from a guess.

Registering a watch

curl -X POST https://api.metera.xyz/v1/watch \
  -H "Authorization: Bearer mk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{
    "type": "solana.can_sign",
    "params": { "signer": "...", "target": "..." },
    "webhookUrl": "https://your-service.example/hooks/metera",
    "minIntervalSeconds": 30
  }'

Returns { id, watchedAddress, webhookSecret } — webhookSecret is shown once, at creation, and never retrievable again, the same convention as an API key. Use it to verify every delivery (below). watchedAddress is the one on-chain address this watch actually subscribes to, derived from your type and params — not necessarily every address in params.

What you can watch

solana.resolve_address       -> watches params.address
solana.can_sign               -> watches params.target
solana.token_risk             -> watches params.address
solana.pool_state             -> watches params.pool
solana.token_account_owner    -> watches params.account

A type outside this list, or a request missing the param it needs, is refused with the reason named — never silently watching the wrong thing.

Managing watches

GET    /v1/watch          -> { "watches": [ ... ] }   (webhookSecret never included)
DELETE /v1/watch/:id      -> 204, or 404 if it is not yours

condition — deciding when to fire

Omit condition and you are notified on every re-verified delivery. Two narrower forms are supported, and both need a real prior delivery to compare against — neither fires on the very first re-verification, since there is nothing yet to say it changed from:

{ "field": "onCurve", "changes": true }
{ "field": "overallRisk", "crosses": 70, "direction": "above" }   // direction: "below" | "above"

minIntervalSeconds — debounce

A trigger for this watch's address is ignored if it arrives within minIntervalSeconds (default 30) of the last delivered notification. This is what keeps a hot account — a busy pool's vault, moving on every trade — from turning every single change into a billed re-verification.

The webhook payload, and verifying it

Every delivery carries an HMAC-SHA256 signature — inside the JSON body, as signature, not a header. Computed over a canonical (sorted-key) serialization of the notification with the signature field itself excluded:

{
  "event":    "watch.fired",
  "watchId":  "...",
  "type":     "solana.can_sign",
  "params":   { "signer": "...", "target": "..." },
  "value":    { ... },              // the re-verified data, same shape get_verified_data returns
  "traceId":  "tr_...",             // audit id for the re-verification that produced this delivery
  "firedAt":  "2026-09-21T00:19:50.903Z",
  "signature":"..."                 // hex HMAC-SHA256, see below
}
// Node — verifying a delivery with your webhookSecret
const crypto = require('crypto')

function canonicalJson(value) {
  if (value === null || typeof value !== 'object') return JSON.stringify(value)
  if (Array.isArray(value)) return '[' + value.map(canonicalJson).join(',') + ']'
  const keys = Object.keys(value).sort()
  return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(value[k])).join(',') + '}'
}

function verify(payload, secret) {
  const { signature, ...rest } = payload
  const expected = crypto.createHmac('sha256', secret).update(canonicalJson(rest)).digest('hex')
  if (!/^[0-9a-f]{64}$/.test(signature || '')) return false
  return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'))
}

Delivery, and what it does not guarantee

Delivery goes through the same SSRF-guarded egress every outbound call on this platform uses — a webhookUrl pointing at a private or reserved address is refused at registration. A delivery attempt that fails (your endpoint is down, times out, or errors) is logged and not retried within this window; the next real trigger tries again. A watch does not backfill missed deliveries.

Billing

Registering, listing, and deleting a watch are free. Each delivered notification is billed exactly like a direct get_verified_data call for that type would be — a trigger that finds nothing worth delivering (the condition did not fire, or the underlying source had nothing to say) charges nothing.

Roadmap

Live today: polling as the trigger — a single-node interval read, bounded latency instead of near-instant, spending no dual-RPC budget on the mere “did anything change” check (that budget is reserved for what actually gets delivered).
Wired, not active: a Geyser/gRPC streaming trigger for lower latency, switched on per deployment once a real caller's use case justifies the added infrastructure cost — the re-verification and billing model above does not change when it does; only how fast a real change is noticed.