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ão true; passe false para 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 retorna checked: 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.