Follow-up to #268, per the scope note there.
Enforcement today (v0.12.263, fa0b1468): one site. src/proxy.ts:2323-2325 builds the proxy x402Client and calls registerSpendPolicyHook before registerExactEvmScheme. No other file calls registerSpendPolicyHook or SpendControl.check().
Paths that sign with the same wallet and never consult it:
src/doctor.ts:409-439 — a second x402Client (L409), EVM/SVM schemes (L410/L426), wrapFetchWithPayment (L435), paid /v1/chat/completions (L438-439) at ~$0.003–$0.01/run (DOCTOR_MODELS L370-379). The proxy's shape minus one line.
src/polymarket/fund.ts:122 — createPaymentPayload() (@blockrun/llm) signs an EIP-3009 authorization to bridge on eip155:8453; L132-133 pays the $0.01 fee via new BlockrunClient({ privateKey }). No SpendControl import.
src/polymarket/orders.ts:433 — clob.createAndPostOrder(). confirm:true (L407), POLYMARKET_MAX_BET_USD (L377) and reserveBet (L429) are per-order notional caps on a separate ledger, not a counterparty check.
src/polymarket/withdraw.ts:147 and src/polymarket/redeem.ts:155 — sendTransaction / sendWalletBatch, same shape.
Net: an allowedPayees list that makes the proxy refuse every other payee does nothing here. docs/configuration.md reads wallet-level; enforcement is proxy-level.
Fix
doctor.ts: registerSpendPolicyHook(x402, new SpendControl()) before L410 — identical to proxy.ts:2325.
- Polymarket paths:
control.check(amount, { payTo, network, asset }) before the signer — CAIP-2 network, contract addresses from constants.ts; !allowed throws SpendPolicyError, nothing is signed. The fee leg inside BlockrunClient needs its own hook or a doc note — @blockrun/llm 3.13.2 exposes none.
- Fail closed: no parseable amount with limits configured → refuse, as
assertSpendPolicyAllows already does.
Tests — wiring-level, mutation-checked like proxy.spend-policy.test.ts: in-memory SpendControl with blockedPayees matching the counterparty; assert the signer is never called on each path. Deleting each check line must turn the test red.
PR to follow.
Follow-up to #268, per the scope note there.
Enforcement today (v0.12.263,
fa0b1468): one site.src/proxy.ts:2323-2325builds the proxyx402Clientand callsregisterSpendPolicyHookbeforeregisterExactEvmScheme. No other file callsregisterSpendPolicyHookorSpendControl.check().Paths that sign with the same wallet and never consult it:
src/doctor.ts:409-439— a secondx402Client(L409), EVM/SVM schemes (L410/L426),wrapFetchWithPayment(L435), paid/v1/chat/completions(L438-439) at ~$0.003–$0.01/run (DOCTOR_MODELSL370-379). The proxy's shape minus one line.src/polymarket/fund.ts:122—createPaymentPayload()(@blockrun/llm) signs an EIP-3009 authorization tobridgeoneip155:8453; L132-133 pays the $0.01 fee vianew BlockrunClient({ privateKey }). NoSpendControlimport.src/polymarket/orders.ts:433—clob.createAndPostOrder().confirm:true(L407),POLYMARKET_MAX_BET_USD(L377) andreserveBet(L429) are per-order notional caps on a separate ledger, not a counterparty check.src/polymarket/withdraw.ts:147andsrc/polymarket/redeem.ts:155—sendTransaction/sendWalletBatch, same shape.Net: an
allowedPayeeslist that makes the proxy refuse every other payee does nothing here.docs/configuration.mdreads wallet-level; enforcement is proxy-level.Fix
doctor.ts:registerSpendPolicyHook(x402, new SpendControl())before L410 — identical toproxy.ts:2325.control.check(amount, { payTo, network, asset })before the signer — CAIP-2 network, contract addresses fromconstants.ts;!allowedthrowsSpendPolicyError, nothing is signed. The fee leg insideBlockrunClientneeds its own hook or a doc note —@blockrun/llm3.13.2 exposes none.assertSpendPolicyAllowsalready does.Tests — wiring-level, mutation-checked like
proxy.spend-policy.test.ts: in-memorySpendControlwithblockedPayeesmatching the counterparty; assert the signer is never called on each path. Deleting each check line must turn the test red.PR to follow.