{"_id":"@agentum/x402-spend-guard","_rev":"4-caceaff7935b6f0d4d2c5e3aa5098a6d","name":"@agentum/x402-spend-guard","dist-tags":{"latest":"1.1.1"},"versions":{"1.0.0":{"name":"@agentum/x402-spend-guard","version":"1.0.0","keywords":["x402","spend-control","budget","agent-payments","usdc","security","agentic-commerce"],"license":"MIT","_id":"@agentum/x402-spend-guard@1.0.0","maintainers":[{"name":"agentum","email":"agentumcomercial@carmozinog.com"}],"homepage":"https://github.com/orionlabsai/x402-spend-guard","bugs":{"url":"https://github.com/orionlabsai/x402-spend-guard/issues"},"bin":{"x402-kill-switch":"kill-switch-cli.js"},"dist":{"shasum":"31e676e51b2391c024904ac6cf82afe26e540c0f","tarball":"https://registry.npmjs.org/@agentum/x402-spend-guard/-/x402-spend-guard-1.0.0.tgz","fileCount":7,"integrity":"sha512-iage/JunDjdmhazyVi2lOgZnh/1hX3CaYzZDNQBJFol0LwSgnMqQaMedCjouPI0zj694r1yUzvbHQSZenxwauw==","signatures":[{"sig":"MEYCIQDNJeb0Qy7pLH1Ri5DGTHBRgaBij5AzTyGTluKgnzehTwIhAK2X/mHapshcGXwlD9KNPlAgM6qBwvhb0hMvxky65WgT","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":25636},"main":"index.js","type":"commonjs","engines":{"node":">=22.5.0"},"gitHead":"3171071d4b94177c83a2fbea229043f407ed0595","scripts":{"test":"node --test"},"_npmUser":{"name":"agentum","email":"agentumcomercial@carmozinog.com"},"repository":{"url":"git+https://github.com/orionlabsai/x402-spend-guard.git","type":"git"},"_npmVersion":"11.19.0","description":"Trava financeira de saída pra agentes x402: teto por transação, teto diário agregado, allowlist de rede/ativo/destinatário/host e kill switch — o que o SDK oficial não cobre (SpendControls é só por transação).","directories":{},"_nodeVersion":"26.7.0","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/x402-spend-guard_1.0.0_1789175643606_0.9714950791947814","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@agentum/x402-spend-guard","version":"1.0.1","keywords":["x402","spend-control","budget","agent-payments","usdc","security","agentic-commerce"],"license":"MIT","_id":"@agentum/x402-spend-guard@1.0.1","maintainers":[{"name":"agentum","email":"agentumcomercial@carmozinog.com"}],"homepage":"https://github.com/orionlabsai/x402-spend-guard","bugs":{"url":"https://github.com/orionlabsai/x402-spend-guard/issues"},"bin":{"x402-kill-switch":"kill-switch-cli.js"},"dist":{"shasum":"92fdd436d36e210501444b4a39e35f128daf3653","tarball":"https://registry.npmjs.org/@agentum/x402-spend-guard/-/x402-spend-guard-1.0.1.tgz","fileCount":7,"integrity":"sha512-YC5BLBVULLZJIkdwGCbLB/ADX9SZniEodl+fyzv1CRniJ1M4SG+8IQTLP5G4C4sC47fR+4yX6wew4cgBkzXkcQ==","signatures":[{"sig":"MEUCIQDssZYW977EbZbfftKK5zDcOMhTdVYBNvqkGdINbyzPQQIgZb6OExWKn56ecO8w1wY+8+VDEYcyDM2o3SYJD0Gd7sM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":26434},"main":"index.js","type":"commonjs","engines":{"node":">=22.5.0"},"gitHead":"494bccaa5aee4da2a6ddef79c3c76ef7101a8824","scripts":{"test":"node --test"},"_npmUser":{"name":"agentum","email":"agentumcomercial@carmozinog.com"},"repository":{"url":"git+https://github.com/orionlabsai/x402-spend-guard.git","type":"git"},"_npmVersion":"11.19.0","description":"Trava financeira de saída pra agentes x402: teto por transação, teto diário agregado, allowlist de rede/ativo/destinatário/host e kill switch — o que o SDK oficial não cobre (SpendControls é só por transação).","directories":{},"_nodeVersion":"26.7.0","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/x402-spend-guard_1.0.1_1789219433033_0.5323206371015823","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@agentum/x402-spend-guard","version":"1.1.0","keywords":["x402","spend-control","budget","agent-payments","usdc","security","agentic-commerce"],"license":"MIT","_id":"@agentum/x402-spend-guard@1.1.0","maintainers":[{"name":"agentum","email":"agentumcomercial@carmozinog.com"}],"homepage":"https://github.com/orionlabsai/x402-spend-guard","bugs":{"url":"https://github.com/orionlabsai/x402-spend-guard/issues"},"bin":{"x402-kill-switch":"kill-switch-cli.js"},"dist":{"shasum":"d0a8de8dbc44fd7744cd93f7a534cb918a806da5","tarball":"https://registry.npmjs.org/@agentum/x402-spend-guard/-/x402-spend-guard-1.1.0.tgz","fileCount":8,"integrity":"sha512-aW8SikMoPS74GRlZIopQnVT+iF9CuvPhKU6IQDOu8nlx4OT2CBdf2Eo5iUKrWJ71Bi67Wz910zZSddf09qg5Lg==","signatures":[{"sig":"MEYCIQCj8p+MlCqg8S3t+QTanmQTt7UGoME7cLEd27dJmEJIoAIhANrogLV9ZGvBCKuVK0nQ9XRbzOdwhly0IZC93Ni4EzGd","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDq1mzUOyaZuCftRGcv8JT4LfuwU3u7f8M0kjzZgZRMNgIgD3+FyBTmKr8rKHY3zxqfItLbWbeMYunHhhGRfNENsy0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":39970},"main":"index.js","type":"commonjs","engines":{"node":">=22.5.0"},"gitHead":"220ce1bb1cd590e4fc88f23f83d6fd008b3977d5","scripts":{"test":"node --test"},"_npmUser":{"name":"agentum","email":"agentumcomercial@carmozinog.com"},"repository":{"url":"git+https://github.com/orionlabsai/x402-spend-guard.git","type":"git"},"_npmVersion":"10.9.8","description":"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).","directories":{},"_nodeVersion":"22.23.2","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/x402-spend-guard_1.1.0_1789737510238_0.6820898820003394","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"_id":"@agentum/x402-spend-guard@1.1.1","bin":{"x402-kill-switch":"kill-switch-cli.js"},"bugs":{"url":"https://github.com/orionlabsai/x402-spend-guard/issues"},"dist":{"shasum":"261137a10fec79aa40ec19cb26c6e3da7770fc04","tarball":"https://registry.npmjs.org/@agentum/x402-spend-guard/-/x402-spend-guard-1.1.1.tgz","fileCount":8,"integrity":"sha512-v7eTyps8o9G69vsR43uLQ8BROwO+HZdj2PMRo5Gi4Aiq9L4+lM3E0gyH4GkMX4Q3ORivt9vOzXzK+lMYrgqn9g==","signatures":[{"sig":"MEUCIGshE2aMdBT1bEPgwiENev1iTaiFjPCQd6yaTjvMEeCLAiEAmlaRiKXNWFRso/iqc3kFlMnGnpLBIaH6u0y50OiW3JY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCi1Xba/Iu75VjOOX6BaAhXcN6FlZxdELt5cNK8mu/v2AIhAI7Ly1NYXx3YJS9Hq8vEFy4nzcuFhWQxpCYmN6JKZyX3"}],"unpackedSize":42514},"main":"index.js","name":"@agentum/x402-spend-guard","type":"commonjs","engines":{"node":">=22.5.0"},"gitHead":"b17a1a32ab5cc10d7db7cac4449afaca61751f2b","license":"MIT","scripts":{"test":"node --test"},"version":"1.1.1","_npmUser":{"name":"agentum","email":"agentumcomercial@carmozinog.com"},"homepage":"https://github.com/orionlabsai/x402-spend-guard","keywords":["x402","spend-control","budget","agent-payments","usdc","security","agentic-commerce"],"repository":{"url":"git+https://github.com/orionlabsai/x402-spend-guard.git","type":"git"},"_npmVersion":"10.9.8","description":"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).","directories":{},"maintainers":[{"name":"agentum","email":"agentumcomercial@carmozinog.com"}],"_nodeVersion":"22.23.2","dependencies":{},"_hasShrinkwrap":false,"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/x402-spend-guard_1.1.1_1789766684653_0.8134332192025897"}}},"time":{"created":"2026-09-12T01:14:03.476Z","modified":"2026-09-18T21:24:44.961Z","1.0.0":"2026-09-12T01:14:03.755Z","1.0.1":"2026-09-12T13:23:53.195Z","1.1.0":"2026-09-18T13:18:30.342Z","1.1.1":"2026-09-18T21:24:44.740Z"},"bugs":{"url":"https://github.com/orionlabsai/x402-spend-guard/issues"},"license":"MIT","homepage":"https://github.com/orionlabsai/x402-spend-guard","keywords":["x402","spend-control","budget","agent-payments","usdc","security","agentic-commerce"],"repository":{"url":"git+https://github.com/orionlabsai/x402-spend-guard.git","type":"git"},"description":"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).","maintainers":[{"name":"agentum","email":"agentumcomercial@carmozinog.com"}],"readme":"# @agentum/x402-spend-guard\n\nTrava financeira de **saída** pra quem constrói um agente que paga via [x402](https://github.com/x402-foundation/x402) — o lado que gasta, não o que vende.\n\n## O problema\n\nO SDK oficial (`@x402/core`) só trava **por transação** (`SpendControls.maxAmountPerPayment`). Não existe:\n- teto agregado por dia,\n- kill switch,\n- allowlist de destinatário (`payTo`) ou host do recurso,\n- log de auditoria persistente entre chamadas.\n\nNem [A2A](https://a2a-protocol.org/latest/topics/extensions/) nem [MCP](https://modelcontextprotocol.io/specification/2025-06-18) preenchem esse gap — nenhum dos dois tem conceito de orçamento/política de gasto do lado do requisitante.\n\n## O que esta lib faz\n\nUma 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 }`.\n\n- Teto por transação **e** teto diário agregado (SQLite nativo via `node:sqlite`, zero dependência nova)\n- 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)\n- Kill switch read-only pro código que gasta — só um CLI separado escreve, com `chmod 444` de fricção extra\n- Fail-closed em tudo: erro interno, JSON corrompido, `accepts[]` vazio/malformado — tudo vira bloqueio, nunca exceção não tratada\n- Log de toda decisão (aprovada ou bloqueada) + do resultado real do envio, separados\n- **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`)\n\nTestado em produção real: o teto/allowlist/kill switch é a mesma trava usada pelo [Payment Agent da AGENTUM](https://agentum.lat) 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.\n\n## Instalação\n\n```bash\nnpm install @agentum/x402-spend-guard\n```\n\n## Uso\n\n```js\nconst { SpendGuard } = require(\"@agentum/x402-spend-guard\");\n\nconst guard = new SpendGuard({\n  maxPerTransactionUnits: 100_000,   // 0,10 USDC (6 casas decimais)\n  dailyCapUnits: 1_000_000,          // 1,00 USDC/dia\n  allowedNetworks: [\"eip155:8453\"],  // Base mainnet\n  allowedAssets: [\"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\"], // USDC na Base\n  allowedPayTo: [\"0xSeuDestinatarioConfiavel...\"],\n  allowedResourceHosts: [\"api.confiavel.com\"],\n});\n\n// depois de receber um 402 de um servidor x402, antes de assinar qualquer coisa:\nconst decision = guard.evaluateAccepts(paymentRequired.accepts, resourceUrl);\n\nif (!decision.allowed) {\n  throw new Error(`Pagamento bloqueado pela política: ${decision.reason}`);\n}\n\n// ... assine e envie o pagamento normalmente (wrapFetchWithPayment, etc) ...\n\n// depois de saber o resultado real do envio:\nguard.logOutcome({ outcome: \"settled\", amountUnits: decision.amountUnits, resourceUrl });\n```\n\nTodas 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\".\n\n## Kill switch\n\n```bash\nnpx x402-kill-switch status\nnpx x402-kill-switch on \"investigando comportamento suspeito\"\nnpx x402-kill-switch off\n# com caminho customizado (senão usa ./data/kill-switch.json):\nnpx x402-kill-switch on \"motivo\" --path /caminho/seguro/kill-switch.json\n```\n\nO 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.\n\n**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.\n\n## Circuit breaker por saúde real de outcome (v1.1.0)\n\nTodo \"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.\n\n```js\nconst guard = new SpendGuard({\n  // ...allowlists de sempre...\n  circuitBreaker: { failureThreshold: 3, cooldownMs: 60_000 }, // opcional -- ausente = comportamento idêntico à v1.0.x\n});\n\nconst decision = guard.evaluateAccepts(paymentRequired.accepts, resourceUrl);\nif (!decision.allowed) {\n  // decision.reason pode ser \"circuit_open\" agora -- 3 outcomes reais\n  // seguidos que não foram \"settled\" pra esse ENDPOINT (host+path), e o cooldown ainda não expirou.\n}\n\n// sempre chamar logOutcome depois do resultado real -- é isso que alimenta o circuit breaker\nguard.logOutcome({ outcome: \"settled\" /* ou \"settle_failed\", \"network_error\", etc */, amountUnits, resourceUrl });\n```\n\n- **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.\n- **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).\n- **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.\n- **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.\n- **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).\n\n## Store customizado\n\nPor 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 })`.\n\n## Riscos residuais conhecidos\n\n- A trava só protege quem chama `evaluateAccepts()` antes de assinar — nada intercepta automaticamente um caminho de saída novo que não a chame.\n- 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).\n- 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.\n- 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.\n- **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.\n- **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.\n- **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.\n- **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.\n- **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- **Não avalia destinatários secundários de split-payment** (ex: `PaymentRequirements.extra.splits`, proposto na [PR #3221](https://github.com/x402-foundation/x402/pull/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](https://github.com/x402-foundation/x402/issues/3170#issuecomment-5646093920) — rastreado aqui, não implementado ainda porque o campo não existe em nenhuma resposta real de servidor x402 hoje.\n\n## Licença\n\nMIT\n","readmeFilename":"README.md"}