# Meridian Meridian is a read-only x402 pay-per-call HTTP API operated by MioDio LLC. Agents call Meridian before they sign. Pre-sign checks return DeFi risk cards from public on-chain reads, a public OFAC SDN exact-match screen, a parameter-drift check on unsigned transactions, a transaction simulation dry-run, and a paid on-chain USDC payment-check. Live origin: https://meridian.miodiollc.com ## What it does NOT do - Never holds keys, does not sign user transactions, and does not move funds. Clients pay USDC via x402 (EIP-3009); Meridian does not custody wallets. A dedicated signing-only key may sign Meridian-Receipt headers. - No post-signing tracking. A paid 2xx response includes a signed Meridian-Receipt header when receipt signing is configured (lookup GET /v1/receipt/:id). The receipt id is a bearer secret. The receipt is not a payment and cannot unlock another call. - Does not broadcast, execute, or guarantee on-chain outcomes. Simulation is a point-in-time dry run against latest state, not a promise of inclusion, gas, or the eventual result. It does not scan mempools or wallet histories. ## Sanctions list source - Exact-match screen of digital-currency addresses from the public U.S. Treasury OFAC SDN list (ITA Consolidated Screening List as official fallback). - The Worker cron (13:15 UTC) loads a small daily JSON extract (SDN_FEED_URL / meridian-sdn-feed) plus last-run.json, not the full Treasury XML. last-run.json must be younger than ~60h (feed heartbeat limit). GET /health.sanctions reports list_updated_at, feed_source, feed_fetched_at, feed_checked_at (the last-run.checked_at the Worker accepted), and stale (>36h since last successful check). ## Paid endpoints (x402 v2 Exact USDC) - GET /v1/risk/:address — ~$0.005 USDC Exact (5000 atomic), network eip155:8453, asset Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x4DA1ED782BD61BaE9AdD5a5CaBbCf32f767f8d7F. Pre-sign check: agents call Meridian before they sign. Example unpaid URL: GET /v1/risk/0x0000000000000000000000000000000000000001. - GET /v1/lido/exit-risk — ~$0.005 USDC Exact (5000 atomic), network eip155:8453, asset Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x4DA1ED782BD61BaE9AdD5a5CaBbCf32f767f8d7F. Pre-sign check: agents call Meridian before they sign. Example unpaid URL: GET /v1/lido/exit-risk?amount=32. - GET /v1/vault/:address — ~$0.005 USDC Exact (5000 atomic), network eip155:8453, asset Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x4DA1ED782BD61BaE9AdD5a5CaBbCf32f767f8d7F. Pre-sign check: agents call Meridian before they sign. Example unpaid URL: GET /v1/vault/0xc1256Ae5FF1cf2719D4937adb3bbCCab2E00A2Ca. - GET /v1/passport/:address — ~$0.02 USDC Exact (20000 atomic), network eip155:8453, asset Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x4DA1ED782BD61BaE9AdD5a5CaBbCf32f767f8d7F. Pre-sign check: agents call Meridian before they sign. Example unpaid URL: GET /v1/passport/0x0000000000000000000000000000000000000001. - GET /v1/sanctions/:address — ~$0.005 USDC Exact (5000 atomic), network eip155:8453, asset Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x4DA1ED782BD61BaE9AdD5a5CaBbCf32f767f8d7F. Pre-sign check: agents call Meridian before they sign. Example unpaid URL: GET /v1/sanctions/0x0000000000000000000000000000000000000001. - GET /v1/passport-sanctions/:address — ~$0.02 USDC Exact (20000 atomic), network eip155:8453, asset Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x4DA1ED782BD61BaE9AdD5a5CaBbCf32f767f8d7F. Pre-sign check: agents call Meridian before they sign. Example unpaid URL: GET /v1/passport-sanctions/0x0000000000000000000000000000000000000001. - POST /v1/sanctions/batch — ~$0.10 USDC Exact (100000 atomic), network eip155:8453, asset Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x4DA1ED782BD61BaE9AdD5a5CaBbCf32f767f8d7F. Pre-sign check: agents call Meridian before they sign. - POST /v1/drift — ~$0.005 USDC Exact (5000 atomic), network eip155:8453, asset Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x4DA1ED782BD61BaE9AdD5a5CaBbCf32f767f8d7F. Pre-sign check: agents call Meridian before they sign. - POST /v1/simulate — ~$0.01 USDC Exact (10000 atomic), network eip155:8453, asset Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x4DA1ED782BD61BaE9AdD5a5CaBbCf32f767f8d7F. Pre-sign check: agents call Meridian before they sign. - POST /v1/payment-check — ~$0.005 USDC Exact (5000 atomic), network eip155:8453, asset Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, payTo 0x4DA1ED782BD61BaE9AdD5a5CaBbCf32f767f8d7F. Pre-sign check: agents call Meridian before they sign. ## Free endpoints (no payment) - GET /health — liveness, version, payment_mode, demo pointers - GET /v1/receipt — scanner receipt and sample 402 paths (unchanged card) - GET /v1/receipt/:id — signed per-call receipt lookup. The id is a bearer secret (do not share). no-store. - GET /.well-known/meridian-receipt-keys.json — Ed25519 public keys for verifying Meridian-Receipt - GET /receipt-verify.ts — public TypeScript snippet for offline receipt verification - GET /v1/payment/status — Meridian's own payment status by tx or payer+nonce (30/min per IP). Does not return receipt_id. On a miss, use paid POST /v1/payment-check. - GET /v1/demo/risk, /v1/demo/vault, /v1/demo/sanctions, /v1/demo/drift, /v1/demo/simulate — fixed public demo cards - GET /.well-known/x402 — machine-readable paid catalog - GET /.well-known/x402list.txt — optional x402-list ownership token (404 when unset) - GET /.well-known/agent-card.json — A2A agent card - GET /robots.txt — crawl allow-list + discovery pointers - GET /llms.txt — this file - GET /openapi.json — OpenAPI 3.1 - GET /docs — human-readable docs - GET /sitemap.xml — HTML page index - GET /terms, /privacy, /disclaimer — legal pages - OPTIONS on paid /v1/* routes — CORS preflight, never 402 ## Verify a Meridian-Receipt - Canonical bytes: recursively sorted-key JSON, no whitespace, every field except `sig`. This is NOT RFC 8785 JCS. - Ed25519-verify those bytes with GET /.well-known/meridian-receipt-keys.json (match kid). Require kty OKP and crv Ed25519. sig is canonical standard base64 or base64url (padding required; Worker receipts use standard base64). Honor not_before. Reject status revoked even without revoked_at. Reject status retired unless retired_at is a valid ISO 8601 UTC instant ending in Z, then accept only issued_at before that time. Reject present-but-invalid timestamps (including +00:00 or a missing Z). revoked_at means do not trust the key. - Also confirm the on-chain USDC Transfer on Base matching receipt.tx, payer, pay_to, and amount. amount/asset/pay_to/price_usd come from settlement or advertised route terms, never the client payment header. - Public TypeScript snippet: GET https://meridian.miodiollc.com/receipt-verify.ts (also inlined below and on /docs). /** * Offline Meridian-Receipt verifier (Node 22+ / Workers WebCrypto). * * Canonical bytes are recursively sorted-key JSON with no whitespace, * excluding `sig`. This is NOT RFC 8785 JSON Canonicalization Scheme (JCS). * * After the signature verifies, also confirm the on-chain USDC Transfer on * Base (eip155:8453) whose tx hash, from, to, and value match receipt.tx, * receipt.payer, receipt.pay_to, and receipt.amount. * * JWKS keys must include kty "OKP" and crv "Ed25519". alg, when present, * must be EdDSA or Ed25519 (live keys use alg "Ed25519"). * * `sig` is strict standard base64 or canonical base64url: padding required, * no whitespace, and re-encoding must match the input exactly. Worker-issued * receipts use standard base64 (`btoa`). * * Timestamps (receipt `issued_at`; key `not_before` / `retired_at` / * `revoked_at`) must be ISO 8601 UTC instants ending in Z. */ export function stableJson(value: unknown): string { if (value == null || typeof value !== "object") return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; const obj = value as Record; const keys = Object.keys(obj).sort(); return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(obj[key])}`).join(",")}}`; } export function receiptCanonicalBytes(receipt: Record): Uint8Array { const unsigned: Record = {}; for (const key of Object.keys(receipt)) { if (key === "sig") continue; unsigned[key] = receipt[key]; } return new TextEncoder().encode(stableJson(unsigned)); } /** ISO 8601 UTC instant ending in Z (optional 1–9 fractional-second digits). */ const ISO_INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?Z$/; export function isIsoInstant(value: unknown): value is string { return typeof value === "string" && ISO_INSTANT_RE.test(value) && !Number.isNaN(Date.parse(value)); } /** Canonical standard base64 or base64url. Re-encoding must match exactly. */ export function decodeStrictB64(text: string): Uint8Array | null { if (typeof text !== "string" || text.length === 0) return null; if (/\s/.test(text)) return null; const isUrl = /^[A-Za-z0-9_-]+={0,2}$/.test(text); const isStd = /^[A-Za-z0-9+/]+={0,2}$/.test(text); if (!isUrl && !isStd) return null; if (text.length % 4 !== 0) return null; const normalized = text.replace(/-/g, "+").replace(/_/g, "/"); let binary: string; try { binary = atob(normalized); } catch { return null; } const reStd = btoa(binary); const reencoded = isUrl ? reStd.replace(/\+/g, "-").replace(/\//g, "_") : reStd; if (reencoded !== text) return null; const out = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i); return out; } export type ReceiptKey = { kid?: unknown; x?: unknown; kty?: unknown; crv?: unknown; alg?: unknown; status?: unknown; not_before?: unknown; retired_at?: unknown; revoked_at?: unknown; }; export type KeyWindowResult = { ok: true } | { ok: false; reason: string }; function timestampField( value: unknown, ): { kind: "absent" } | { kind: "invalid" } | { kind: "ok"; ms: number } { if (value === undefined) return { kind: "absent" }; if (!isIsoInstant(value)) return { kind: "invalid" }; return { kind: "ok", ms: Date.parse(value) }; } export function keyCoversIssuedAt(key: ReceiptKey, issuedAt: string): boolean { return keyWindowCheck(key, issuedAt).ok; } export function keyWindowCheck(key: ReceiptKey, issuedAt: string): KeyWindowResult { if (!isIsoInstant(issuedAt)) return { ok: false, reason: "invalid_issued_at" }; const issued = Date.parse(issuedAt); const notBefore = timestampField(key.not_before); const retiredAt = timestampField(key.retired_at); const revokedAt = timestampField(key.revoked_at); if (notBefore.kind === "invalid" || retiredAt.kind === "invalid" || revokedAt.kind === "invalid") { return { ok: false, reason: "invalid_key_timestamp" }; } if (key.status === "revoked" || revokedAt.kind === "ok") { return { ok: false, reason: "key_revoked" }; } if (key.status === "retired" && retiredAt.kind !== "ok") { return { ok: false, reason: "retired_without_retired_at" }; } if (key.status !== undefined && key.status !== "active" && key.status !== "retired") { return { ok: false, reason: "invalid_key_status" }; } if (notBefore.kind === "ok" && issued < notBefore.ms) { return { ok: false, reason: "key_outside_validity_window" }; } if (retiredAt.kind === "ok" && issued >= retiredAt.ms) { return { ok: false, reason: "key_outside_validity_window" }; } return { ok: true }; } export type VerifyReceiptResult = | { ok: true } | { ok: false; reason: string }; const KEY_ALG_OK = new Set(["Ed25519", "EdDSA"]); export async function verifyMeridianReceipt( receipt: Record, keysDoc: { keys?: unknown }, ): Promise { if (typeof receipt.sig !== "string" || !receipt.sig) { return { ok: false, reason: "missing_sig" }; } if (receipt.alg !== "Ed25519") return { ok: false, reason: "unsupported_alg" }; if (typeof receipt.kid !== "string" || !receipt.kid) { return { ok: false, reason: "missing_kid" }; } if (typeof receipt.issued_at !== "string") { return { ok: false, reason: "missing_issued_at" }; } if (!Array.isArray(keysDoc.keys)) { return { ok: false, reason: "invalid_keys" }; } const rawKeys = (keysDoc.keys ?? []) as unknown[]; if (rawKeys.some((item) => item == null || typeof item !== "object")) { return { ok: false, reason: "invalid_keys" }; } const keys = rawKeys as ReceiptKey[]; const matches = keys.filter((item) => item.kid === receipt.kid); if (matches.length > 1) return { ok: false, reason: "duplicate_kid" }; const key = matches[0]; if (!key || typeof key.x !== "string") return { ok: false, reason: "unknown_kid" }; if (key.kty !== "OKP") return { ok: false, reason: "bad_kty" }; if (key.crv !== "Ed25519") return { ok: false, reason: "bad_crv" }; if (key.alg !== undefined && !KEY_ALG_OK.has(String(key.alg))) { return { ok: false, reason: "bad_alg" }; } const window = keyWindowCheck(key, receipt.issued_at); if (!window.ok) return window; const sig = decodeStrictB64(receipt.sig); if (!sig) return { ok: false, reason: "bad_sig_encoding" }; try { const verifyKey = await crypto.subtle.importKey( "jwk", { kty: "OKP", crv: "Ed25519", x: key.x, ext: true }, { name: "Ed25519" }, false, ["verify"], ); const ok = await crypto.subtle.verify( { name: "Ed25519" }, verifyKey, sig, receiptCanonicalBytes(receipt), ); return ok ? { ok: true } : { ok: false, reason: "bad_signature" }; } catch { return { ok: false, reason: "verify_error" }; } } ## How to pay 1. GET or POST a paid path without PAYMENT-SIGNATURE → HTTP 402. The JSON body and the PAYMENT-REQUIRED header are the same x402 v2 PaymentRequired object (x402Version, accepts, resource). Empty or invalid POST bodies without a payment header still 402; body validation runs only after a payment header is present. 2. Sign an EIP-3009 authorization for the accepts[0] amount / asset / payTo / network. 3. Retry with PAYMENT-SIGNATURE. Successful calls return JSON plus PAYMENT-RESPONSE and, when receipt signing is configured, a signed Meridian-Receipt header. Settlement runs only when the handler status is below 400, so paid-but-invalid input is 400 and is not charged. ## Links - Terms: https://meridian.miodiollc.com/terms - Privacy: https://meridian.miodiollc.com/privacy - Disclaimer: https://meridian.miodiollc.com/disclaimer - Docs: https://meridian.miodiollc.com/docs - Discovery: https://meridian.miodiollc.com/.well-known/x402 - Receipt keys: https://meridian.miodiollc.com/.well-known/meridian-receipt-keys.json - Receipt verifier snippet: https://meridian.miodiollc.com/receipt-verify.ts - Receipt lookup: https://meridian.miodiollc.com/v1/receipt/:id - Payment status: https://meridian.miodiollc.com/v1/payment/status - Agent card: https://meridian.miodiollc.com/.well-known/agent-card.json - Robots: https://meridian.miodiollc.com/robots.txt - Verified on PayAPI Market: https://payapi.market/api/meridian-by-miodio (PayAPI verified Meridian on 2026-10-17 with a real paid canary call to /v1/sanctions). ## Operator MioDio LLC, corp@miodiollc.com, 755 Monroe Rd, Unit 470067, Sanford, FL 32747 (Florida LLC)