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-Verifiedtrue / false
  • X-RelayGuard-Outcomemet · absent · not_converged · divergence · unavailable
  • X-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 Conflictdivergence: 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 Unavailableretryable: either not_converged (one concrete answer vs. null) or quorum unavailable (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-Signatureed25519=<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-Signaturesha256=<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.