The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the SendCheck — EVM address verification (x402) listing page.
Verify before you pay. A zero-dependency client for the free
GET /validate
pre-check on the SendCheck x402 API —
the 10-line guard that stops your agent from signing a payment to a mistyped,
mismatched, or tampered settlement address.
When an x402 client pays a service, it pays the settlement address the service
told it about. A single wrong character (a case-flip, a dropped digit, a
substituted address) and the USDC goes somewhere else forever. /validate is
the free, no-key, no-account pre-check: is this address even a well-formed,
correctly-cased EVM address before you put real money on it?
Since v0.2.0 the package does one more thing: it verifies the service's signed service card — an ES256 JWS over the payTo and the per-route maxPrice that the worker re-signs at every deploy — so a client that pinned the public key once can confirm the challenge's payTo cryptographically, before paying. That is the reference implementation of the drift-proof check (the "signed service card" from the address-drift article).
SendCheck's paid endpoints cost $0.01 (one chain) or $0.05 (all five chains)
via x402 — USDC on Base, no account, no API key. But most "is this address
garbage?" checks don't need chain data: the EIP-55 checksum alone catches the
most common, most expensive failures (typos, dropped characters, tampered
casing). So /validate is free, and this package makes calling it one line.
Pre-payment address checks have a hole: the "expected payTo" a client compares against usually comes from the same discovery doc it just fetched — so a compromised service can point you at its own new address, and for a service that rotates its address often, every remembered address is stale before the check finishes.
SendCheck closes that hole by signing its payment details. The worker signs
the settlement address and the per-route max prices with a stable ES256 (EC
P-256) key at every deploy, and serves the signed block { jws, key } in three
places:
SendCheck signs its payment details with a stable key at every deploy, so any client that has pinned that key once can cryptographically confirm — before paying — that the address in the payment challenge is one we signed; first contact is still trust-on-first-use.
What the signature buys: key continuity for returning, stateful clients — a compromised worker/DNS/CDN cannot silently redirect your payments, and a rotated key is a loud event you can flag. What it does not buy (we under-claim on purpose): first-visit authenticity (that is TOFU), protection for ephemeral agents with no pin, or a price below the signed maxPrice.
The signed payload also carries a per-route maxPrice, so verifyChallenge
catches a challenge that quietly raises the price above what the key signed.
verifyChallenge checks the challenge's payTo against the signed one and the
challenge price against the signed maxPrice for the challenged route. Failure
reasons: signature_invalid, challenge_payto_mismatch, price_exceeded,
route_not_signed, expired, iat_future, service_mismatch, kid_mismatch,
malformed.
GitHub's npm registry asks for any valid GitHub token even on public reads — put
//npm.pkg.github.com/:_authToken=<your GH token> in your .npmrc (classic PAT or
fine-grained with Packages:Read works). No token handy? Install from the public repo:
Requires Node 18+ (uses global fetch). Zero dependencies. MIT.
CommonJS:
Typical wiring inside an x402 client:
If you already normalize addresses on your side and just want the result object (no throw):
verifyBeforePay(address, opts?)address — EVM address, 0x + 40 hex.opts.baseUrl — override the API host (default: the live worker).opts.fetchFn — inject a fetch implementation (tests / proxies).opts.throwOnError — default true; pass false to always get the result back.Behavior notes:
0x+40-hex fails the local format check and
throws (or returns checked: false) without sending a request — you never
pay a round trip for garbage input.status: "plain" = valid address, all-lowercase or all-uppercase (no case
information). That's fine to pay; the response includes the normalized
EIP-55 form.status: "mismatch" = the casing does not match the address hash — usually
a typo or tampering. This is the one to treat as hard-fail.looksLikeAddress(addr) — offline format check, no network.verifyAttestation(attestation, opts?)Verify a signed block { jws, key } (ES256 JWS over a canonical-JSON payload).
Pure + async (WebCrypto); no network. Returns
{ valid, reason?, payload, keyId, signedAt, expiresAt, warning? }.
opts: origin, expectedPayTo, challengePayTo, route,
challengeMaxAmount (atomic USDC string), now (unix seconds, for tests).
Time windows: exp hard-reject with a 60s clock-skew grace; iat may be up to
60s in the future; an iat older than 30 days is valid but returns
warning: "stale". reason codes: malformed, header_mismatch,
kid_mismatch, signature_invalid, service_mismatch, payto_mismatch,
challenge_payto_mismatch, route_not_signed, price_exceeded, expired,
iat_future.
checkService(origin, opts?)Fetch <origin>/.well-known/x402, verify its signed card against the doc's own
advertised payTo, and report whether a pinned key changed. Returns
{ ok, origin, payTo, keyId, signedAt, expiresAt, warning?, attestation, keyChanged, endpoints: [{resource, amount}] }. opts.fetchFn (inject fetch),
opts.pinnedKey ({kid} or {kid,x,y}), opts.throwOnError. keyChanged is
the loud rotation signal.
verifyChallenge(challenge, attestation, opts?)Bind a 402 challenge to a signed card. challenge is the decoded 402
declaration ({ resource: { url }, accepts: [{ payTo, maxAmountRequired }] })
— decode the base64 PAYMENT-REQUIRED header with
JSON.parse(Buffer.from(hdr, "base64").toString("utf8")). The route is derived
from resource.url. Returns the verifyAttestation result.
ATTESTATION_EXTENSION — "x-sendcheck-attestation", the extension namethe signed block rides in under inside the 402 challenge's extensions map.
canonicalJson(value) — RFC-8785-style canonical JSON (recursively sortedkeys) — exported for anyone re-implementing the signer.
SendCheckVerifyError — thrown on non-valid results; err.result carriesthe full result object.
Free. No key. No account. JSON in, JSON out. The paid siblings — POST /check
($0.01, one chain: wallet-vs-contract, activity, balances, verdict) and
POST /deep ($0.05, all five chains + wrong-network detection) — use x402
(USDC on Base). Machine-readable docs:
llms.txt and
openapi.json
on the same host.
SendCheck is a one-person studio (Pennyforge). The worker runs on Cloudflare
Workers; the address engine is MIT and public
here. If the free endpoint ever
moves, opts.baseUrl is the only thing you change.
MIT — see LICENSE.