Skip to content

feat(policy): counterparty allow/deny by payTo (refuse WHO, not just HOW MUCH) - #7

Open
twzrd-sol wants to merge 1 commit into
cascade-protocol:mainfrom
twzrd-sol:twzrd/counterparty-policy
Open

feat(policy): counterparty allow/deny by payTo (refuse WHO, not just HOW MUCH)#7
twzrd-sol wants to merge 1 commit into
cascade-protocol:mainfrom
twzrd-sol:twzrd/counterparty-policy

Conversation

@twzrd-sol

Copy link
Copy Markdown

Spend limits answer how much may I pay. There is currently no way to answer whom may I pay at all — so an operator who knows a specific payTo is unsafe has no way to express that, short of not using the proxy.

x402-proxy config set denyPayTo 34w53Ukhf...,0xabc...
x402-proxy config set allowPayTo BJGdsDXJ...     # empty string clears the list

How it works

Implemented as a PaymentPolicy through the existing client.registerPolicy, so it behaves exactly like the spend-limit policies already in resolve-wallet.ts: pre-sign. When no acceptable option remains the policy throws, the client never builds a payment payload, and nothing is signed.

No new dependencies. Zero behaviour change when neither list is configured.

Decisions worth a look

  • deny wins over allow when an address appears in both — a denial seemed the safer reading of contradictory config.
  • EVM 0x addresses match case-insensitively (hex checksumming varies); Solana base58 matches case-sensitively, because lower-casing a base58 key yields a different key. Both directions are tested, including the trap where a lower-cased Solana address must not match.
  • An empty requirements list passes through rather than throwing, so this policy never turns "the server offered nothing" into "counterparty refused".
  • The error names every refused address and the config set command to fix it, so a failure is actionable rather than opaque.

Wiring

Applied at the three sites that build a client from config — fetch, mcp, serve — so the setting holds wherever payments happen rather than in one command.

Verification

  • pnpm test: 114 → 124 passing (9 → 10 files); 10 new cases in src/lib/counterparty-policy.test.ts
  • pnpm type-check: clean
  • biome check: clean

Happy to adjust

If you'd rather this were a single payToPolicy field, or scoped per-network, or named differently, say so and I'll rework it. I kept it in the spendLimit* shape on the assumption that matching the existing config idiom matters more than novelty.

One follow-up I deliberately left out of this PR: a pluggable policy module hook, so an operator could point at their own risk logic rather than maintaining a static list. That's a bigger surface and a separate security conversation, so it seemed wrong to bundle. Glad to open it separately if useful.

…HOW MUCH)

Spend limits answer "how much may I pay". There is currently no way to answer
"whom may I pay at all" — so an operator who knows a specific payTo is unsafe
has no way to express that, short of not using the proxy.

This adds `allowPayTo` / `denyPayTo` in exactly the existing spend-limit shape:

  x402-proxy config set denyPayTo 34w53Ukhf...,0xabc...
  x402-proxy config set allowPayTo BJGdsDXJ...      # empty string clears

Implemented as a PaymentPolicy via the existing `client.registerPolicy`, so it
is pre-sign like the spend-limit policies: when no acceptable option remains the
policy throws, the client never builds a payment payload, and nothing is signed.

Details worth reviewing:
- `deny` wins over `allow` when an address is in both — a denial is the safer
  reading of contradictory config.
- EVM `0x` addresses match case-insensitively (hex checksumming varies);
  Solana base58 matches case-sensitively, because lower-casing a base58 key
  yields a different key. Both are covered by tests.
- An empty requirements list passes through rather than throwing, so this policy
  never converts "nothing offered" into "counterparty refused".
- The throw names every refused address plus the `config set` command, so the
  failure is actionable rather than opaque.
- No new dependencies. Zero behaviour change when neither list is configured.

Wired through the three call sites that build a client from config (fetch, mcp,
serve) so the setting applies wherever payments happen, not just one command.

Tests: 10 new cases in src/lib/counterparty-policy.test.ts. Package suite goes
114 -> 124 passing (9 -> 10 files); `pnpm type-check` clean; `biome check`
clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant