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.accountA 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 yourscondition — 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
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.