SendCheck
Verificações pré-envio de endereço EVM: pré-verificação gratuita, verificação de $0,01, varredura profunda de $0,05. x402: pagamento é autenticação.
Documentação
sendcheck-verify
Verifique antes de pagar. Um cliente sem dependências para a
GET /validate
pré-verificação gratuita na API SendCheck x402 —
a proteção de 10 linhas que impede seu agente de assinar um pagamento para um
endereço de liquidação digitado errado, incompatível ou adulterado.
Quando um cliente x402 paga um serviço, ele paga o endereço de liquidação que o
serviço informou. Um único caractere errado (uma troca de maiúsculas/minúsculas, um dígito omitido, um
endereço substituído) e o USDC vai para outro lugar para sempre. /validate é
a pré-verificação gratuita, sem chave, sem conta: este endereço é sequer um
endereço EVM bem formado e com maiúsculas/minúsculas corretas antes de você colocar dinheiro real nele?
Desde a v0.2.0, o pacote faz mais uma coisa: ele verifica o cartão de serviço assinado do serviço — um JWS ES256 sobre o payTo e o maxPrice por rota que o worker re-assina a cada deploy — para que um cliente que fixou a chave pública uma vez possa confirmar criptograficamente o payTo do desafio, antes de pagar. Essa é a implementação de referência da verificação à prova de desvio (o "cartão de serviço assinado" do artigo sobre desvio de endereço).
Por que isso existe
Os endpoints pagos do SendCheck custam $0,01 (uma chain) ou $0,05 (todas as cinco chains)
via x402 — USDC na Base, sem conta, sem chave de API. Mas a maioria das verificações de "este endereço é
lixo?" não precisa de dados de chain: o checksum EIP-55 sozinho captura as
falhas mais comuns e mais caras (erros de digitação, caracteres omitidos, maiúsculas/minúsculas
adulteradas). Então, /validate é gratuito, e este pacote torna a chamada uma linha.
O cartão de serviço assinado (v0.2.0)
As verificações de endereço pré-pagamento têm uma falha: o "payTo esperado" com o qual um cliente compara geralmente vem do mesmo documento de descoberta que ele acabou de buscar — então um serviço comprometido pode apontar você para o próprio endereço novo, e para um serviço que rotaciona o endereço com frequência, todo endereço lembrado está desatualizado antes de a verificação terminar.
O SendCheck fecha essa falha assinando seus detalhes de pagamento. O worker assina
o endereço de liquidação e os preços máximos por rota com uma chave ES256 estável (EC
P-256) a cada deploy, e serve o bloco assinado { jws, key } em três
lugares:
<origin>/.well-known/x402 → top-level "attestation"
<origin>/openapi.json → info["x-attestation"]
the 402 challenge itself → extensions["x-sendcheck-attestation"]
O SendCheck assina seus detalhes de pagamento com uma chave estável a cada deploy, então qualquer cliente que fixou essa chave uma vez pode confirmar criptograficamente — antes de pagar — que o endereço no desafio de pagamento é um que assinamos; o primeiro contato ainda é trust-on-first-use.
O que a assinatura compra: continuidade de chave para clientes recorrentes e com estado — um worker/DNS/CDN comprometido não pode redirecionar silenciosamente seus pagamentos, e uma chave rotacionada é um evento alto que você pode sinalizar. O que ela não compra (nós subdeclaramos de propósito): autenticidade na primeira visita (isso é TOFU), proteção para agentes efêmeros sem pin, ou um preço abaixo do maxPrice assinado.
O payload assinado também carrega um maxPrice por rota, então verifyChallenge
captura um desafio que aumenta silenciosamente o preço acima do que a chave assinou.
A fiação à prova de desvio
import { checkService, verifyChallenge, ATTESTATION_EXTENSION } from "@pennyforgeorg/sendcheck-verify";
const origin = "https://api.pennyforge.org";
// 1) Once: pin the public key (first contact = trust-on-first-use).
const first = await checkService(origin); // { ok, keyId, key: JWK, payTo, … }
// persist { kid: first.keyId, x: first.key.x, y: first.key.y } somewhere durable
// 2) Every call: confirm the card still verifies and the key has not rotated.
const svc = await checkService(origin, { pinnedKey: storedPin });
if (svc.keyChanged) { /* loud: the key rotated — inspect before trusting */ }
if (!svc.ok) throw new Error("service card not valid: " + svc.attestation.reason);
// 3) At payment time: bind the 402 CHALLENGE to the signed card.
// (challenge = decoded base64 PAYMENT-REQUIRED header, or the 402 JSON body)
const verdict = await verifyChallenge(challenge, svc.block, { origin });
if (!verdict.valid) throw new Error("challenge not signed: " + verdict.reason);
// …now sign the EIP-3009 payment to challenge.accepts[0].payTo.
verifyChallenge verifica o payTo do desafio contra o assinado e o
preço do desafio contra o maxPrice assinado para a rota desafiada. Motivos
de falha: signature_invalid, challenge_payto_mismatch, price_exceeded,
route_not_signed, expired, iat_future, service_mismatch, kid_mismatch,
malformed.
Instalação
# GitHub npm registry (v0.2.0 published 2026-09-08)
npm install @pennyforgeorg/sendcheck-verify --registry https://npm.pkg.github.com
O registro npm do GitHub pede qualquer token GitHub válido mesmo em leituras públicas — coloque
//npm.pkg.github.com/:_authToken=<your GH token> no seu .npmrc (PAT clássico ou
fine-grained com Packages:Read funciona). Sem token à mão? Instale do repositório público:
npm install github:pennyforgeorg/sendcheck-verify
Requer Node 18+ (usa o fetch global). Zero dependências. MIT.
Início rápido
import { verifyBeforePay } from "@pennyforgeorg/sendcheck-verify";
// Throws SendCheckVerifyError unless the address passes the free pre-check.
await verifyBeforePay("0x9504A5939AB5be2B2B1F8beA7D7ebeCcd96c485D");
// …then sign your x402 payment.
CommonJS:
const { verifyBeforePay } = require("@pennyforgeorg/sendcheck-verify");
Fiação típica dentro de um cliente x402:
import { verifyBeforePay } from "@pennyforgeorg/sendcheck-verify";
import { wrapFetchWithPayment } from "x402-fetch"; // or your own x402 stack
const pay = wrapFetchWithPayment(fetch, { wallet });
async function call(serviceUrl, address, body) {
await verifyBeforePay(address); // free — catch bad addresses here
const res = await pay(serviceUrl, { // paid only when the address is clean
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
return res.json();
}
Se você já normaliza endereços do seu lado e só quer o objeto de resultado (sem lançar exceção):
const r = await verifyBeforePay(addr, { throwOnError: false });
// { valid, status: "valid" | "plain" | "mismatch" | "format" | "network" | "http",
// message, normalized, checked }
API
verifyBeforePay(address, opts?)
address— endereço EVM,0x+ 40 hex.opts.baseUrl— substitui o host da API (padrão: o worker ativo).opts.fetchFn— injeta uma implementação de fetch (testes / proxies).opts.throwOnError— padrãotrue; passefalsepara sempre obter o resultado de volta.
Notas de comportamento:
- Uma string que nem sequer é
0x+40-hex falha na verificação local de formato e lança (ou retornachecked: false) sem enviar uma requisição — você nunca paga uma ida e volta por entrada inválida. status: "plain"= endereço válido, todo em minúsculas ou todo em maiúsculas (sem informação de maiúsculas/minúsculas). Tudo bem pagar; a resposta inclui a forma EIP-55 normalizada.status: "mismatch"= as maiúsculas/minúsculas não correspondem ao hash do endereço — geralmente um erro de digitação ou adulteração. Este é o caso para tratar como falha grave.
looksLikeAddress(addr) — verificação de formato offline, sem rede.
verifyAttestation(attestation, opts?)
Verifica um bloco assinado { jws, key } (JWS ES256 sobre um payload JSON canônico).
Puro + assíncrono (WebCrypto); sem rede. Retorna
{ valid, reason?, payload, keyId, signedAt, expiresAt, warning? }.
opts: origin, expectedPayTo, challengePayTo, route,
challengeMaxAmount (string atômica de USDC), now (segundos unix, para testes).
Janelas de tempo: exp rejeição rígida com uma margem de 60s de tolerância de relógio; iat pode estar até
60s no futuro; um iat mais antigo que 30 dias é válido, mas retorna
warning: "stale". Códigos reason: 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?)
Busca <origin>/.well-known/x402, verifica seu cartão assinado contra o próprio
payTo anunciado no documento e informa se uma chave fixada mudou. Retorna
{ ok, origin, payTo, keyId, signedAt, expiresAt, warning?, attestation, keyChanged, endpoints: [{resource, amount}] }. opts.fetchFn (injetar fetch),
opts.pinnedKey ({kid} ou {kid,x,y}), opts.throwOnError. keyChanged é
o sinal alto de rotação.
verifyChallenge(challenge, attestation, opts?)
Vincula um desafio 402 a um cartão assinado. challenge é a declaração 402
decodificada ({ resource: { url }, accepts: [{ payTo, maxAmountRequired }] })
— decodifique o cabeçalho PAYMENT-REQUIRED em base64 com
JSON.parse(Buffer.from(hdr, "base64").toString("utf8")). A rota é derivada
de resource.url. Retorna o resultado verifyAttestation.
ATTESTATION_EXTENSION — "x-sendcheck-attestation", o nome da extensão
sob a qual o bloco assinado viaja dentro do mapa extensions do desafio 402.
canonicalJson(value) — JSON canônico estilo RFC-8785 (chaves ordenadas
recursivamente) — exportado para quem quiser reimplementar o assinante.
SendCheckVerifyError — lançado em resultados não válidos; err.result carrega
o objeto de resultado completo.
O endpoint, em termos simples
GET https://api.pennyforge.org/validate?address=0x…
→ 200 { "valid": true, "status": "valid", "message": "Valid EIP-55 checksum.",
"normalized": "0x…" }
Gratuito. Sem chave. Sem conta. JSON entra, JSON sai. Os irmãos pagos — POST /check
($0,01, uma chain: wallet-vs-contract, atividade, saldos, veredito) e
POST /deep ($0,05, todas as cinco chains + detecção de rede errada) — usam x402
(USDC na Base). Documentação legível por máquina:
llms.txt e
openapi.json
no mesmo host.
Quem opera
O SendCheck é um estúdio de uma pessoa (Pennyforge). O worker roda no Cloudflare
Workers; o motor de endereços é MIT e público
aqui. Se o endpoint gratuito algum dia
mudar, opts.baseUrl é a única coisa que você altera.
Licença
MIT — veja LICENSE.