npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agentum/x402-spend-guard

v1.1.1

Published

Trava financeira de saída pra agentes x402: teto por transação, teto diário agregado, allowlist de rede/ativo/destinatário/host, kill switch e circuit breaker por saúde real de outcome — o que o SDK oficial não cobre (SpendControls é só por transação).

Readme

@agentum/x402-spend-guard

Trava financeira de saída pra quem constrói um agente que paga via x402 — o lado que gasta, não o que vende.

O problema

O SDK oficial (@x402/core) só trava por transação (SpendControls.maxAmountPerPayment). Não existe:

  • teto agregado por dia,
  • kill switch,
  • allowlist de destinatário (payTo) ou host do recurso,
  • log de auditoria persistente entre chamadas.

Nem A2A nem MCP preenchem esse gap — nenhum dos dois tem conceito de orçamento/política de gasto do lado do requisitante.

O que esta lib faz

Uma camada fora do caminho de decisão do código que assina a transação — ele nunca vê nem controla os limites, só recebe { allowed, reason }.

  • Teto por transação e teto diário agregado (SQLite nativo via node:sqlite, zero dependência nova)
  • Allowlist obrigatória de rede, ativo, destinatário e host do recurso — o construtor recusa rodar sem elas (nunca assume "aceita tudo" por omissão)
  • Kill switch read-only pro código que gasta — só um CLI separado escreve, com chmod 444 de fricção extra
  • Fail-closed em tudo: erro interno, JSON corrompido, accepts[] vazio/malformado — tudo vira bloqueio, nunca exceção não tratada
  • Log de toda decisão (aprovada ou bloqueada) + do resultado real do envio, separados
  • Circuit breaker opcional por saúde real de outcome (v1.1.0+) — pausa automaticamente pagamentos pra um endpoint (host+path) que os últimos pagamentos reais confirmaram estar falhando, sem depender de polling de liveness (GET /health)

Testado em produção real: o teto/allowlist/kill switch é a mesma trava usada pelo Payment Agent da AGENTUM desde 2026-09-05, com pagamentos reais em Base mainnet. O circuit breaker (v1.1.0) é novo e ainda não passou por produção — testado com concorrência real entre processos (não só chamadas no mesmo processo) antes do release, mas sem histórico de uso real ainda.

Instalação

npm install @agentum/x402-spend-guard

Uso

const { SpendGuard } = require("@agentum/x402-spend-guard");

const guard = new SpendGuard({
  maxPerTransactionUnits: 100_000,   // 0,10 USDC (6 casas decimais)
  dailyCapUnits: 1_000_000,          // 1,00 USDC/dia
  allowedNetworks: ["eip155:8453"],  // Base mainnet
  allowedAssets: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"], // USDC na Base
  allowedPayTo: ["0xSeuDestinatarioConfiavel..."],
  allowedResourceHosts: ["api.confiavel.com"],
});

// depois de receber um 402 de um servidor x402, antes de assinar qualquer coisa:
const decision = guard.evaluateAccepts(paymentRequired.accepts, resourceUrl);

if (!decision.allowed) {
  throw new Error(`Pagamento bloqueado pela política: ${decision.reason}`);
}

// ... assine e envie o pagamento normalmente (wrapFetchWithPayment, etc) ...

// depois de saber o resultado real do envio:
guard.logOutcome({ outcome: "settled", amountUnits: decision.amountUnits, resourceUrl });

Todas as opções de configuração são obrigatórias e validadas no construtor — uma allowlist vazia ou ausente por engano lança erro na hora, em vez de silenciosamente virar "aceita qualquer coisa".

Kill switch

npx x402-kill-switch status
npx x402-kill-switch on "investigando comportamento suspeito"
npx x402-kill-switch off
# com caminho customizado (senão usa ./data/kill-switch.json):
npx x402-kill-switch on "motivo" --path /caminho/seguro/kill-switch.json

O kill switch só é lido pelo SpendGuard — nenhuma função de escrita existe na lib em si. A única forma de ligar/desligar é rodando o CLI manualmente. Isso garante que o próprio código que gasta dinheiro nunca tem, à disposição, uma função capaz de se autodesbloquear.

Importante: isso protege contra escrita, não contra deleção. Se o arquivo do kill switch for apagado (não editado, apagado), isKillSwitchActive() trata isso como "nunca foi ativado" — ou seja, apagar um kill switch ATIVO o desliga silenciosamente. Garanta que o processo que gasta dinheiro não tenha permissão de escrita no diretório onde esse arquivo vive, não só no arquivo em si.

Circuit breaker por saúde real de outcome (v1.1.0)

Todo "circuit breaker"/"failover" que existe hoje pro x402 monitora se um endpoint responde (polling de liveness). Nenhum monitora se ele entrega quando alguém tenta pagar de verdade. Esta lib deriva a saúde do host a partir do que você já reporta em logOutcome() — sem outcome real, sem dado, sem custo extra.

const guard = new SpendGuard({
  // ...allowlists de sempre...
  circuitBreaker: { failureThreshold: 3, cooldownMs: 60_000 }, // opcional -- ausente = comportamento idêntico à v1.0.x
});

const decision = guard.evaluateAccepts(paymentRequired.accepts, resourceUrl);
if (!decision.allowed) {
  // decision.reason pode ser "circuit_open" agora -- 3 outcomes reais
  // seguidos que não foram "settled" pra esse ENDPOINT (host+path), e o cooldown ainda não expirou.
}

// sempre chamar logOutcome depois do resultado real -- é isso que alimenta o circuit breaker
guard.logOutcome({ outcome: "settled" /* ou "settle_failed", "network_error", etc */, amountUnits, resourceUrl });
  • Estados: closed (normal) → open (depois de N falhas consecutivas, bloqueia) → half_open (cooldown expirou, deixa passar 1 sonda de teste) → closed de novo se a sonda vier settled, ou open de novo (reinicia o cooldown) se falhar.
  • Por host + path (v1.1.1) — /rota-a e /rota-b do mesmo domínio têm saúde INDEPENDENTE; a mesma rota com query string diferente (?id=1 vs ?id=2) continua compartilhando saúde, é o mesmo endpoint. Corrigido depois de tentar usar o failover de verdade entre uma rota real e seu espelho no mesmo domínio (v1.1.0 agrupava por host inteiro, o que fazia o "espelho" nunca poder ser escolhido — tinha sempre a mesma saúde da rota principal).
  • Sempre opt-in: sem circuitBreaker na config, nada disso roda — só o teto/allowlist de sempre. Consultar saúde manualmente com guard.getEndpointHealth(url) funciona mesmo sem habilitar o bloqueio automático.
  • Failover mínimo: guard.pickHealthyResource([urlPrincipal, urlEspelho, ...]) devolve a primeira URL cujo endpoint (host+path) não está open, ou null se todas estiverem — funciona mesmo que os espelhos estejam no MESMO domínio da rota principal. A lib nunca descobre espelhos sozinha, só ajuda a escolher entre os que você já conhece.
  • Retrocompatível de propósito: um store customizado escrito antes desta versão (sem .health) continua funcionando exatamente como antes — o circuit breaker simplesmente nunca bloqueia nesse caso (best-effort, nunca lança).

Store customizado

Por padrão, SpendGuard cria seu próprio SpendStore (SQLite em ./data/x402-spend-guard.sqlite). Se precisar compartilhar o mesmo ledger entre múltiplos hosts, implemente a mesma interface (reserve/getSpentToday/logDecision/isKillSwitchActive) sobre Redis/Postgres e passe via new SpendGuard({ ..., store: meuStoreCustomizado }).

Riscos residuais conhecidos

  • A trava só protege quem chama evaluateAccepts() antes de assinar — nada intercepta automaticamente um caminho de saída novo que não a chame.
  • Kill switch é garantia de código + chmod 444, não isolamento de SO real — protege contra escrita, não contra deleção do arquivo (ver seção "Kill switch" acima).
  • Sem rollback depois que a reserva é feita — se o envio falhar depois, o valor já contou pro teto diário (decisão deliberada: nunca estourar o teto é mais importante que permitir retry fácil). Isso também significa que erro/timeout repetido consome o teto diário sem gastar dinheiro de verdade — se seu agente tende a falhar bastante, o teto pode esgotar por tentativa, não por gasto real.
  • O SQLite local do SpendStore padrão cobre múltiplos processos num host só, não múltiplos hosts. Sob concorrência real (múltiplos processos), o SQLite espera o lock soltar (PRAGMA busy_timeout) em vez de falhar na hora — mas ainda serializa, não paraleliza: throughput alto concorrente pode ficar lento, não incorreto.
  • Se você criar mais de um SpendGuard no mesmo processo sem passar store explícito pra cada um, os dois vão compartilhar o mesmo arquivo SQLite padrão (./data/x402-spend-guard.sqlite) e portanto o mesmo teto diário acumulado — provavelmente não é o que você quer. Passe um store com dbPath próprio pra cada guard se precisar de políticas independentes.
  • Circuit breaker é heurística simples, não um SLA: threshold fixo de falhas consecutivas (sem backoff exponencial, sem distinguir "servidor fora do ar" de "seu próprio saldo/config está errado" — qualquer outcome diferente de settled conta igual). Um provedor genuinamente saudável pode ficar bloqueado por alguns minutos por uma sequência de erro transitório do SEU lado (rede, nonce, etc), não necessariamente do lado dele.
  • Estado de saúde é local ao SpendStore (mesmo SQLite do teto diário) — múltiplos processos no mesmo host compartilham (se apontarem pro mesmo dbPath), múltiplos hosts, não.
  • Chave de saúde é host+pathname, não um "endpoint canônico": uma rota parametrizada no path (ex: /users/123 vs /users/456, se você usar esse estilo de URL) vira uma chave DIFERENTE por valor, e as falhas nunca se acumulam o suficiente pra abrir o circuito dessa rota. Funciona bem pra rotas com parâmetros só em query string (/users?id=123 — ignorado na chave) ou pra espelhos com paths fixos (/rota vs /rota-mirror), que foi o caso real que motivou a feature.
  • Upgrade de v1.1.0 pra v1.1.1: a chave mudou de host para host+pathname — um circuito que já estivesse open sob a chave antiga (só host) não é migrado, reabre como closed até a próxima falha real. Sem risco de gasto indevido (é só a trava de saúde relaxando, o teto/allowlist continuam intactos), mas vale saber se você rodou a v1.1.0 por mais que algumas horas antes de atualizar.
  • Não avalia destinatários secundários de split-payment (ex: PaymentRequirements.extra.splits, proposto na PR #3221 do x402 core — ainda não é spec oficial hoje). checkOption() só confere o payTo/amount do nível principal de cada opção em accepts[]; se um esquema de pagamento dividido virar padrão, uma perna secundária (taxa de plataforma, referral) poderia sair da allowlist de destinatário ou empurrar o gasto agregado além do teto sem a guard perceber. Achado real, levantado por @whawk46 — rastreado aqui, não implementado ainda porque o campo não existe em nenhuma resposta real de servidor x402 hoje.

Licença

MIT