Verifying responses & webhooks
RelayGuard exposes integrity metadata and cryptographic signatures so your application can trust what it received and authenticate what we send you. This page covers the three surfaces: quorum response semantics, signed audit receipts, and webhook signatures.
1. Quorum response semantics
Security Mode responses carry integrity metadata in HTTP headers — never in the JSON-RPC body — so strict clients (ethers, viem, cast) are unaffected:
X-RelayGuard-Verified—true/falseX-RelayGuard-Outcome—met·absent·not_converged·divergence·unavailableX-RelayGuard-Confidence,X-RelayGuard-Quorum(agreeing count),X-RelayGuard-Required,X-RelayGuard-Independent-Groups
Map the HTTP status to behaviour:
- 200 — quorum met (verified), or all providers agree the value is absent (returns
null). Safe to proceed. - 409 Conflict — divergence: two or more independent providers returned different concrete answers. Do not proceed — the read is contested. Surface or alert; do not retry blindly.
- 503 Service Unavailable — retryable: either
not_converged(one concrete answer vs.null) or quorumunavailable(too few healthy independent providers). Back off and retry.
Rule of thumb: 409 = stop, 503 = retry. Never treat a 409/503 as a successful read.
2. Signed audit receipts
When receipt signing is enabled, each Security Mode response includes a tamper-evident, third-party-verifiable receipt:
X-RelayGuard-Receipt— base64url-encoded canonical JSON (the exact signed bytes)X-RelayGuard-Signature—ed25519=<base64 signature>
Fetch the public key once from GET /receipts/public-key, then verify the signature over the base64url-decoded receipt bytes. The receipt binds the requestId and a resultHash (SHA-256 of the returned result), so you can prove later exactly what was returned and that RelayGuard verified it.
// Node (tweetnacl / noble-ed25519 style)
const receiptB64 = res.headers.get("X-RelayGuard-Receipt");
const sig = res.headers.get("X-RelayGuard-Signature").replace(/^ed25519=/, "");
const message = Buffer.from(receiptB64, "base64url"); // exact signed bytes
const ok = ed25519.verify(
Buffer.from(sig, "base64"),
message,
publicKeyBytes, // from /receipts/public-key
);
if (!ok) throw new Error("receipt signature invalid");3. Webhook signature verification
Every outbound webhook (integrity / health alerts) is signed with HMAC-SHA256 keyed by your webhook secret:
X-RelayGuard-Signature—sha256=<lowercase hex HMAC-SHA256(rawBody)>X-RelayGuard-Event— the event category
Compute the HMAC over the raw request body with your secret and compare in constant time. Reject if it doesn't match.
// Node (Express raw body)
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(header || "");
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Note: X-RelayGuard-Signature is used on two different surfaces with distinct prefixes — sha256= (HMAC) on outbound webhooks, and ed25519= on RPC response receipts. Always check the prefix.