@absol-labs/agent 0.11.0 → 0.12.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -50
- package/dist/capability/invocation-capability.d.ts +2 -2
- package/dist/capability/probe-capability.d.ts +225 -0
- package/dist/capability/probe-capability.d.ts.map +1 -0
- package/dist/capability/probe-capability.js +248 -0
- package/dist/capability/probe-capability.js.map +1 -0
- package/dist/cli/args.d.ts +11 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +23 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/claim.js +1 -1
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/close.d.ts.map +1 -1
- package/dist/cli/commands/close.js +35 -3
- package/dist/cli/commands/close.js.map +1 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.d.ts.map +1 -1
- package/dist/cli/commands/invoke.js +2 -2
- package/dist/cli/commands/invoke.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +15 -2
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/open.d.ts.map +1 -1
- package/dist/cli/commands/open.js +58 -4
- package/dist/cli/commands/open.js.map +1 -1
- package/dist/cli/commands/pay.d.ts +19 -0
- package/dist/cli/commands/pay.d.ts.map +1 -0
- package/dist/cli/commands/pay.js +355 -0
- package/dist/cli/commands/pay.js.map +1 -0
- package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
- package/dist/cli/commands/reclaim-unverified.js +3 -2
- package/dist/cli/commands/reclaim-unverified.js.map +1 -1
- package/dist/cli/commands/reclaim.d.ts +23 -2
- package/dist/cli/commands/reclaim.d.ts.map +1 -1
- package/dist/cli/commands/reclaim.js +184 -16
- package/dist/cli/commands/reclaim.js.map +1 -1
- package/dist/cli/commands/refund.d.ts +13 -0
- package/dist/cli/commands/refund.d.ts.map +1 -0
- package/dist/cli/commands/refund.js +224 -0
- package/dist/cli/commands/refund.js.map +1 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +10 -1
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/streams.js +8 -12
- package/dist/cli/commands/streams.js.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js +2 -1
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli/config.d.ts +44 -26
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +88 -38
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/deps.d.ts +33 -0
- package/dist/cli/deps.d.ts.map +1 -1
- package/dist/cli/deps.js +37 -1
- package/dist/cli/deps.js.map +1 -1
- package/dist/cli/errors.d.ts.map +1 -1
- package/dist/cli/errors.js +21 -1
- package/dist/cli/errors.js.map +1 -1
- package/dist/cli/escape-hatch.d.ts +6 -4
- package/dist/cli/escape-hatch.d.ts.map +1 -1
- package/dist/cli/escape-hatch.js +6 -2
- package/dist/cli/escape-hatch.js.map +1 -1
- package/dist/cli/listings.d.ts +18 -0
- package/dist/cli/listings.d.ts.map +1 -1
- package/dist/cli/listings.js +22 -12
- package/dist/cli/listings.js.map +1 -1
- package/dist/cli/preflight.d.ts +20 -14
- package/dist/cli/preflight.d.ts.map +1 -1
- package/dist/cli/preflight.js +34 -20
- package/dist/cli/preflight.js.map +1 -1
- package/dist/cli/privy.d.ts.map +1 -1
- package/dist/cli/privy.js +4 -2
- package/dist/cli/privy.js.map +1 -1
- package/dist/cli/reclaimability.d.ts +82 -0
- package/dist/cli/reclaimability.d.ts.map +1 -0
- package/dist/cli/reclaimability.js +80 -0
- package/dist/cli/reclaimability.js.map +1 -0
- package/dist/cli/run.d.ts +2 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +40 -11
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/scan-range.d.ts +2 -2
- package/dist/cli/scan-range.js +2 -2
- package/dist/cli/status.d.ts +3 -1
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +15 -3
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/wallet.d.ts +1 -1
- package/dist/cli/wallet.js +2 -2
- package/dist/cli/wallet.js.map +1 -1
- package/dist/discovery/rate-band.d.ts +74 -0
- package/dist/discovery/rate-band.d.ts.map +1 -0
- package/dist/discovery/rate-band.js +114 -0
- package/dist/discovery/rate-band.js.map +1 -0
- package/dist/escrow-generation.d.ts +81 -0
- package/dist/escrow-generation.d.ts.map +1 -0
- package/dist/escrow-generation.js +76 -0
- package/dist/escrow-generation.js.map +1 -0
- package/dist/frameworks/agentkit.d.ts +8 -4
- package/dist/frameworks/agentkit.d.ts.map +1 -1
- package/dist/frameworks/agentkit.js +27 -10
- package/dist/frameworks/agentkit.js.map +1 -1
- package/dist/frameworks/eliza.d.ts +3 -3
- package/dist/frameworks/eliza.d.ts.map +1 -1
- package/dist/frameworks/eliza.js +15 -6
- package/dist/frameworks/eliza.js.map +1 -1
- package/dist/frameworks/langchain.d.ts.map +1 -1
- package/dist/frameworks/langchain.js +8 -0
- package/dist/frameworks/langchain.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +83 -31
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +180 -27
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +111 -44
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +439 -155
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/probe-auth.d.ts +112 -0
- package/dist/gateway/probe-auth.d.ts.map +1 -0
- package/dist/gateway/probe-auth.js +127 -0
- package/dist/gateway/probe-auth.js.map +1 -0
- package/dist/gateway/signed-rate-bounds.d.ts +51 -0
- package/dist/gateway/signed-rate-bounds.d.ts.map +1 -0
- package/dist/gateway/signed-rate-bounds.js +94 -0
- package/dist/gateway/signed-rate-bounds.js.map +1 -0
- package/dist/gateway/x402-opener-mode.d.ts +215 -0
- package/dist/gateway/x402-opener-mode.d.ts.map +1 -0
- package/dist/gateway/x402-opener-mode.js +639 -0
- package/dist/gateway/x402-opener-mode.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +52 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +228 -37
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/x402-tools.d.ts +104 -0
- package/dist/mcp/x402-tools.d.ts.map +1 -0
- package/dist/mcp/x402-tools.js +434 -0
- package/dist/mcp/x402-tools.js.map +1 -0
- package/dist/network.d.ts +119 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +199 -0
- package/dist/network.js.map +1 -0
- package/dist/sdk/invoke.d.ts +4 -0
- package/dist/sdk/invoke.d.ts.map +1 -1
- package/dist/sdk/invoke.js +2 -5
- package/dist/sdk/invoke.js.map +1 -1
- package/dist/wallet/autonomous-wallet-broker.d.ts +3 -0
- package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet-broker.js +27 -23
- package/dist/wallet/autonomous-wallet-broker.js.map +1 -1
- package/dist/wallet/autonomous-wallet-protocol.d.ts +8 -2
- package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet-protocol.js +9 -2
- package/dist/wallet/autonomous-wallet-protocol.js.map +1 -1
- package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet.js +3 -2
- package/dist/wallet/autonomous-wallet.js.map +1 -1
- package/dist/wallet/broker-escrows.d.ts +222 -0
- package/dist/wallet/broker-escrows.d.ts.map +1 -0
- package/dist/wallet/broker-escrows.js +36 -0
- package/dist/wallet/broker-escrows.js.map +1 -0
- package/dist/wallet/lifecycle.d.ts +21 -5
- package/dist/wallet/lifecycle.d.ts.map +1 -1
- package/dist/wallet/lifecycle.js +32 -12
- package/dist/wallet/lifecycle.js.map +1 -1
- package/dist/wallet/privy-broker-server.d.ts.map +1 -1
- package/dist/wallet/privy-broker-server.js +31 -26
- package/dist/wallet/privy-broker-server.js.map +1 -1
- package/dist/wallet/privy-session-broker.d.ts +14 -5
- package/dist/wallet/privy-session-broker.d.ts.map +1 -1
- package/dist/wallet/privy-session-broker.js +29 -17
- package/dist/wallet/privy-session-broker.js.map +1 -1
- package/dist/wallet/privy-session-provider.d.ts +3 -1
- package/dist/wallet/privy-session-provider.d.ts.map +1 -1
- package/dist/wallet/privy-session-provider.js +5 -3
- package/dist/wallet/privy-session-provider.js.map +1 -1
- package/dist/wallet/provider.d.ts.map +1 -1
- package/dist/wallet/provider.js +15 -8
- package/dist/wallet/provider.js.map +1 -1
- package/dist/x402/facilitator.d.ts +3 -30
- package/dist/x402/facilitator.d.ts.map +1 -1
- package/dist/x402/facilitator.js +0 -27
- package/dist/x402/facilitator.js.map +1 -1
- package/dist/x402-buyer/guarded-fetch.d.ts +51 -0
- package/dist/x402-buyer/guarded-fetch.d.ts.map +1 -0
- package/dist/x402-buyer/guarded-fetch.js +147 -0
- package/dist/x402-buyer/guarded-fetch.js.map +1 -0
- package/dist/x402-buyer/index.d.ts +10 -0
- package/dist/x402-buyer/index.d.ts.map +1 -0
- package/dist/x402-buyer/index.js +10 -0
- package/dist/x402-buyer/index.js.map +1 -0
- package/dist/x402-buyer/pay.d.ts +135 -0
- package/dist/x402-buyer/pay.d.ts.map +1 -0
- package/dist/x402-buyer/pay.js +336 -0
- package/dist/x402-buyer/pay.js.map +1 -0
- package/dist/x402-opener/abi.d.ts +684 -0
- package/dist/x402-opener/abi.d.ts.map +1 -0
- package/dist/x402-opener/abi.js +880 -0
- package/dist/x402-opener/abi.js.map +1 -0
- package/dist/x402-opener/facilitator-entry.d.ts +3 -0
- package/dist/x402-opener/facilitator-entry.d.ts.map +1 -0
- package/dist/x402-opener/facilitator-entry.js +37 -0
- package/dist/x402-opener/facilitator-entry.js.map +1 -0
- package/dist/x402-opener/facilitator-server.d.ts +180 -0
- package/dist/x402-opener/facilitator-server.d.ts.map +1 -0
- package/dist/x402-opener/facilitator-server.js +696 -0
- package/dist/x402-opener/facilitator-server.js.map +1 -0
- package/dist/x402-opener/index.d.ts +14 -0
- package/dist/x402-opener/index.d.ts.map +1 -0
- package/dist/x402-opener/index.js +14 -0
- package/dist/x402-opener/index.js.map +1 -0
- package/dist/x402-opener/listing-resolver.d.ts +55 -0
- package/dist/x402-opener/listing-resolver.d.ts.map +1 -0
- package/dist/x402-opener/listing-resolver.js +125 -0
- package/dist/x402-opener/listing-resolver.js.map +1 -0
- package/dist/x402-opener/refund-relay.d.ts +240 -0
- package/dist/x402-opener/refund-relay.d.ts.map +1 -0
- package/dist/x402-opener/refund-relay.js +1240 -0
- package/dist/x402-opener/refund-relay.js.map +1 -0
- package/dist/x402-opener/relayer-queue.d.ts +14 -0
- package/dist/x402-opener/relayer-queue.d.ts.map +1 -0
- package/dist/x402-opener/relayer-queue.js +18 -0
- package/dist/x402-opener/relayer-queue.js.map +1 -0
- package/dist/x402-opener/scheme.d.ts +217 -0
- package/dist/x402-opener/scheme.d.ts.map +1 -0
- package/dist/x402-opener/scheme.js +749 -0
- package/dist/x402-opener/scheme.js.map +1 -0
- package/dist/x402-opener/terms.d.ts +105 -0
- package/dist/x402-opener/terms.d.ts.map +1 -0
- package/dist/x402-opener/terms.js +212 -0
- package/dist/x402-opener/terms.js.map +1 -0
- package/dist/zktls/reclaim.d.ts +2 -2
- package/dist/zktls/t2-delivery-proof.d.ts +6 -6
- package/docs/agent-layer.md +3 -2
- package/docs/autonomous-privy-wallet.md +188 -0
- package/docs/cli.md +184 -31
- package/docs/langchain.md +5 -0
- package/docs/mcp-reference.md +75 -5
- package/docs/probe-capability.md +191 -0
- package/docs/quickstart.md +36 -1
- package/docs/threat-model.md +20 -0
- package/docs/x402.md +628 -595
- package/package.json +19 -17
- package/src/capability/probe-capability.ts +355 -0
- package/src/cli/args.ts +29 -0
- package/src/cli/commands/claim.ts +1 -1
- package/src/cli/commands/close.ts +36 -3
- package/src/cli/commands/invoke.ts +5 -2
- package/src/cli/commands/mcp.ts +18 -2
- package/src/cli/commands/open.ts +75 -4
- package/src/cli/commands/pay.ts +442 -0
- package/src/cli/commands/reclaim-unverified.ts +8 -2
- package/src/cli/commands/reclaim.ts +252 -14
- package/src/cli/commands/refund.ts +277 -0
- package/src/cli/commands/status.ts +17 -1
- package/src/cli/commands/streams.ts +10 -14
- package/src/cli/commands/watch.ts +7 -1
- package/src/cli/config.ts +139 -52
- package/src/cli/deps.ts +74 -1
- package/src/cli/errors.ts +24 -1
- package/src/cli/escape-hatch.ts +19 -5
- package/src/cli/listings.ts +43 -10
- package/src/cli/preflight.ts +36 -21
- package/src/cli/privy.ts +4 -2
- package/src/cli/reclaimability.ts +154 -0
- package/src/cli/run.ts +54 -11
- package/src/cli/scan-range.ts +2 -2
- package/src/cli/status.ts +14 -1
- package/src/cli/wallet.ts +2 -2
- package/src/discovery/rate-band.ts +172 -0
- package/src/escrow-generation.ts +135 -0
- package/src/frameworks/agentkit.ts +45 -11
- package/src/frameworks/eliza.ts +27 -5
- package/src/frameworks/langchain.ts +14 -0
- package/src/gateway/caller-auth-gateway.ts +320 -68
- package/src/gateway/http-server.ts +629 -220
- package/src/gateway/probe-auth.ts +191 -0
- package/src/gateway/signed-rate-bounds.ts +164 -0
- package/src/gateway/x402-opener-mode.ts +974 -0
- package/src/index.ts +82 -19
- package/src/mcp/server.ts +319 -42
- package/src/mcp/x402-tools.ts +621 -0
- package/src/network.ts +302 -0
- package/src/sdk/invoke.ts +6 -5
- package/src/wallet/autonomous-wallet-broker.ts +46 -20
- package/src/wallet/autonomous-wallet-protocol.ts +11 -2
- package/src/wallet/autonomous-wallet.ts +4 -2
- package/src/wallet/broker-escrows.ts +66 -0
- package/src/wallet/lifecycle.ts +53 -18
- package/src/wallet/privy-broker-server.ts +35 -25
- package/src/wallet/privy-session-broker.ts +58 -25
- package/src/wallet/privy-session-provider.ts +7 -3
- package/src/wallet/provider.ts +20 -15
- package/src/x402/facilitator.ts +3 -43
- package/src/x402-buyer/guarded-fetch.ts +213 -0
- package/src/x402-buyer/index.ts +9 -0
- package/src/x402-buyer/pay.ts +517 -0
- package/src/x402-opener/abi.ts +881 -0
- package/src/x402-opener/facilitator-entry.ts +45 -0
- package/src/x402-opener/facilitator-server.ts +1014 -0
- package/src/x402-opener/index.ts +13 -0
- package/src/x402-opener/listing-resolver.ts +202 -0
- package/src/x402-opener/refund-relay.ts +1743 -0
- package/src/x402-opener/relayer-queue.ts +18 -0
- package/src/x402-opener/scheme.ts +1133 -0
- package/src/x402-opener/terms.ts +300 -0
- package/dist/gateway/x402-mode.d.ts +0 -123
- package/dist/gateway/x402-mode.d.ts.map +0 -1
- package/dist/gateway/x402-mode.js +0 -212
- package/dist/gateway/x402-mode.js.map +0 -1
- package/dist/x402/delivery-oracle.d.ts +0 -108
- package/dist/x402/delivery-oracle.d.ts.map +0 -1
- package/dist/x402/delivery-oracle.js +0 -174
- package/dist/x402/delivery-oracle.js.map +0 -1
- package/dist/x402/facilitator-server-entry.d.ts +0 -3
- package/dist/x402/facilitator-server-entry.d.ts.map +0 -1
- package/dist/x402/facilitator-server-entry.js +0 -27
- package/dist/x402/facilitator-server-entry.js.map +0 -1
- package/dist/x402/facilitator-server.d.ts +0 -73
- package/dist/x402/facilitator-server.d.ts.map +0 -1
- package/dist/x402/facilitator-server.js +0 -305
- package/dist/x402/facilitator-server.js.map +0 -1
- package/dist/x402/payment-requirements.d.ts +0 -106
- package/dist/x402/payment-requirements.d.ts.map +0 -1
- package/dist/x402/payment-requirements.js +0 -138
- package/dist/x402/payment-requirements.js.map +0 -1
- package/dist/x402/relayer.d.ts +0 -41
- package/dist/x402/relayer.d.ts.map +0 -1
- package/dist/x402/relayer.js +0 -64
- package/dist/x402/relayer.js.map +0 -1
- package/dist/x402/resource-index.d.ts +0 -75
- package/dist/x402/resource-index.d.ts.map +0 -1
- package/dist/x402/resource-index.js +0 -126
- package/dist/x402/resource-index.js.map +0 -1
- package/dist/x402/verified-delivery-facilitator.d.ts +0 -285
- package/dist/x402/verified-delivery-facilitator.d.ts.map +0 -1
- package/dist/x402/verified-delivery-facilitator.js +0 -423
- package/dist/x402/verified-delivery-facilitator.js.map +0 -1
- package/src/gateway/x402-mode.ts +0 -350
- package/src/x402/delivery-oracle.ts +0 -285
- package/src/x402/facilitator-server-entry.ts +0 -35
- package/src/x402/facilitator-server.ts +0 -445
- package/src/x402/payment-requirements.ts +0 -231
- package/src/x402/relayer.ts +0 -122
- package/src/x402/resource-index.ts +0 -228
- package/src/x402/verified-delivery-facilitator.ts +0 -744
package/docs/x402.md
CHANGED
|
@@ -1,519 +1,598 @@
|
|
|
1
1
|
# Metrik and x402
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
3
|
+
> **Status.** This package implements **x402 v2 `exact` payments that settle
|
|
4
|
+
> into a `StreamEscrowV2` stream**, through the ownerless
|
|
5
|
+
> `X402StreamOpenerFactory` (metrik-contracts#48). It ships a facilitator
|
|
6
|
+
> service (`metrik-x402-opener-facilitator`) and an opt-in x402 v2 mode on the
|
|
7
|
+
> seller gateway. **Both are OFF by default** and have not yet run in
|
|
8
|
+
> production. The factory is deployed and Sourcify-verified on Base Sepolia at
|
|
9
|
+
> `0x43DA0D9602f7fc5ac0bb90461e39b033EB493d24` (implementation
|
|
10
|
+
> `0xbD8B7B2F49Fdb954bda6bE5ce22218d44345DC43`, block 47337415); everything
|
|
11
|
+
> here is testnet only.
|
|
7
12
|
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
> The separate Metrik-native `402` flow (scheme `streamproof-verified-stream`,
|
|
14
|
+
> header `X-METRIK-PAYMENT`) described at the end is **not** x402 and is
|
|
15
|
+
> unchanged.
|
|
16
|
+
|
|
17
|
+
## The hard rule: an opener `payTo` is settled ONLY by the Metrik opener facilitator
|
|
18
|
+
|
|
19
|
+
**Read this before running any of it.** The x402 `payTo` in this mode is the
|
|
20
|
+
address of an `X402StreamOpener`. Every stock x402 `exact` facilitator settles
|
|
21
|
+
by calling `USDC.transferWithAuthorization(to = payTo)` directly. Pointed at an
|
|
22
|
+
opener, that transfer succeeds and **strands 100% of the payer's payment in the
|
|
23
|
+
opener permanently**: the opener deliberately has no sweep or "claim stray
|
|
24
|
+
funds" path (one would be exploitable, because a used and a cancelled EIP-3009
|
|
25
|
+
nonce are indistinguishable on-chain). The same happens if anyone who sees the
|
|
26
|
+
signed payload submits it to USDC first. See metrik-contracts
|
|
27
|
+
`evm/docs/x402-opener.md`.
|
|
28
|
+
|
|
29
|
+
So, in this package, and for anyone integrating it:
|
|
30
|
+
|
|
31
|
+
1. **Only the Metrik opener facilitator may settle an opener `payTo`.** It
|
|
32
|
+
calls `factory.deployAndSettle(terms, authorization)`, which pulls the
|
|
33
|
+
payment into the opener and opens the stream in the same transaction. Its
|
|
34
|
+
`GET /supported` advertises a marker —
|
|
35
|
+
`kinds[].extra.metrikStreamOpener = {settlement: "x402-stream-opener",
|
|
36
|
+
factory, escrow, usdc, chainId, minAmount, receiptTimeoutSeconds, refundRelay?}` — so a resource server can tell it apart
|
|
37
|
+
from a stock facilitator. `refundRelay: true` means the same service also
|
|
38
|
+
runs the gasless refund relay (see below).
|
|
39
|
+
2. **The gateway refuses to start** unless its configured facilitator's
|
|
40
|
+
`/supported` carries that marker for exactly the gateway's factory, escrow,
|
|
41
|
+
token and chain. It re-checks the marker on a timer (default every 300 s)
|
|
42
|
+
and **fails closed** whenever it cannot prove it: `503
|
|
43
|
+
x402-facilitator-unavailable`, no `PAYMENT-REQUIRED`, nothing verified,
|
|
44
|
+
nothing served, nothing settled anywhere else. A facilitator that is
|
|
45
|
+
unreachable when a payment arrives produces `402`, never a free call.
|
|
46
|
+
3. **Never advertise an opener `payTo` to third-party facilitators or discovery
|
|
47
|
+
catalogs** (the x402 Bazaar extension or any other). The gateway registers
|
|
48
|
+
no discovery extension and never submits its route to any catalog. Only
|
|
49
|
+
when the operator configures `METRIK_GATEWAY_X402V2_SCHEMA` does the 402
|
|
50
|
+
_declare_ an input/output schema, in the standard `extensions.bazaar` shape,
|
|
51
|
+
so a client can read what to send; the only facilitator that ever sees the
|
|
52
|
+
payment is the Metrik opener facilitator, and it catalogs nothing. Do not
|
|
53
|
+
register the route with a discovery service.
|
|
54
|
+
4. **Authenticate an opener only as `factory.predict(terms) == payTo`** (or
|
|
55
|
+
`factory.isOpener(address)`), never by code or codehash: anyone can clone the
|
|
56
|
+
implementation outside the factory with byte-identical code. The
|
|
57
|
+
facilitator recomputes `predict` on-chain for every payment.
|
|
58
|
+
5. **The signed payment never leaves the gateway→facilitator path.** The
|
|
59
|
+
gateway strips `PAYMENT-SIGNATURE` (and `X-PAYMENT`) before proxying to the
|
|
60
|
+
seller's upstream.
|
|
61
|
+
|
|
62
|
+
## How a payment flows
|
|
13
63
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
| | **Verified stream** (`src/x402/facilitator.ts`) | **x402 `exact` + delivery gate** (this document's subject) |
|
|
24
|
-
| ------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- |
|
|
25
|
-
| Protocol | Metrik-native, scheme `streamproof-verified-stream` | **Standard x402 v1, scheme `exact`** |
|
|
26
|
-
| Header | `X-METRIK-PAYMENT` | `X-PAYMENT` / `X-PAYMENT-RESPONSE` |
|
|
27
|
-
| Works with `x402-fetch`? | No | **Yes, untouched** |
|
|
28
|
-
| Money | Escrowed in `StreamEscrowV2`, released per verified second | One EIP-3009 transfer, payer -> payee |
|
|
29
|
-
| If the service stops delivering | Unearned escrow is **recoverable** by the buyer | The payment is **prevented**, never recovered |
|
|
30
|
-
| Buyer protection | Strong | **Strictly weaker — see below** |
|
|
31
|
-
|
|
32
|
-
**The x402 path is weaker buyer protection than the stream, and that is not a
|
|
33
|
-
detail to bury.** It holds none of the buyer's money, so it can only stop a
|
|
34
|
-
payment that has not happened yet. Money already paid through it is gone,
|
|
35
|
-
exactly as it would be through any other facilitator. The stream remains the
|
|
36
|
-
only path where "the service stopped delivering" and "you get your money back"
|
|
37
|
-
are the same sentence.
|
|
38
|
-
|
|
39
|
-
What the gate buys is reach: an agent that already speaks x402 gets a meaningful
|
|
40
|
-
safety property for free, with no SDK, no escrow and no Metrik-specific code.
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## The unlock: nothing constrains _why_ a facilitator says no
|
|
45
|
-
|
|
46
|
-
The x402 facilitator interface is `POST /verify`, `POST /settle`,
|
|
47
|
-
`GET /supported`. `/verify` answers with:
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
VerifyResponse = { isValid: boolean, invalidReason?: ErrorReason, payer?: string }
|
|
64
|
+
```
|
|
65
|
+
buyer (@x402/fetch) gateway (x402 mode) opener facilitator chain
|
|
66
|
+
│ GET /resource ───────────▶ 402 + PAYMENT-REQUIRED
|
|
67
|
+
│ payTo = factory.predict(terms)
|
|
68
|
+
│ PAYMENT-SIGNATURE ─────────▶ POST /verify ───────────────▶ all checks below
|
|
69
|
+
│ upstream request (status < 400?)
|
|
70
|
+
│ POST /settle (bearer) ──────▶ deployAndSettle ────────────▶ USDC → opener → StreamEscrowV2
|
|
71
|
+
│ 200 + body + PAYMENT-RESPONSE ◀── receipt shows the stream ◀─────────────────────────── StreamOpened / StreamOpenedFor
|
|
51
72
|
```
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
- **Terms.** One listing's immutable terms are `{operator, serviceRef,
|
|
75
|
+
ratePerSecond, maxDuration}`; the opener for them lives at
|
|
76
|
+
`factory.predict(terms)` whether or not it is deployed yet. The terms travel
|
|
77
|
+
in `PaymentRequirements.extra.metrikStreamOpener` (with the factory address);
|
|
78
|
+
the payer's EIP-3009 signature binds `to = payTo`, so it commits to them.
|
|
79
|
+
- **Price = deposit.** Each paid request opens one stream with `deposit =
|
|
80
|
+
price`, on the listing's terms. The escrow records the **opener** as the
|
|
81
|
+
stream's buyer; the opener records the payer (`payerOf(streamId)`).
|
|
82
|
+
- **Earning.** Unchanged: the operator earns `ratePerSecond × verified
|
|
83
|
+
seconds`, capped at the deposit, claimed with `claimWithProof` against the
|
|
84
|
+
oracle's checkpoints, minus the 0.5% protocol fee.
|
|
85
|
+
- **Settle after serve.** The gateway settles only when the upstream answered
|
|
86
|
+
`< 400`, and before any upstream byte reaches the buyer (the reference
|
|
87
|
+
`@x402/*` middleware order). An upstream `>= 400` submits nothing, so no
|
|
88
|
+
funds move. A failed settlement sends no upstream body.
|
|
89
|
+
|
|
90
|
+
### What the facilitator checks (every payment, fail closed)
|
|
91
|
+
|
|
92
|
+
Standard `exact` checks are delegated to the official `@x402/evm`
|
|
93
|
+
`ExactEvmScheme` verifier (signature, `to == payTo`, `value == amount`,
|
|
94
|
+
validity window, asset contract, a simulation of the transfer). The stock
|
|
95
|
+
scheme is given a signer that cannot broadcast, so its direct-transfer settle
|
|
96
|
+
path is structurally unreachable. On top of it:
|
|
97
|
+
|
|
98
|
+
| Check | Refusal reason |
|
|
99
|
+
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
100
|
+
| x402 v2, scheme `exact`, the configured CAIP-2 network | `metrik_opener_unsupported_*` |
|
|
101
|
+
| `payload.accepted` equals the requirements exactly | `metrik_opener_accepted_requirements_mismatch` |
|
|
102
|
+
| Asset transfer method is EIP-3009 (Permit2 and everything else refused) | `metrik_opener_asset_transfer_method_not_eip3009` |
|
|
103
|
+
| Not an ERC-6492 (counterfactual smart wallet) signature | `metrik_opener_erc6492_signature_unsupported` |
|
|
104
|
+
| Asset is allowlisted and equals `factory.usdc()` | `metrik_opener_asset_not_allowed` |
|
|
105
|
+
| Terms are well formed; the factory named is the configured one | `metrik_opener_invalid_terms` / `_factory_mismatch` |
|
|
106
|
+
| `0 < amount <= ratePerSecond × maxDuration` | `metrik_opener_invalid_amount` / `_amount_exceeds_rate_times_duration` |
|
|
107
|
+
| `payTo == factory.predict(terms)`, read on-chain | `metrik_opener_pay_to_not_predicted_opener` |
|
|
108
|
+
| `(operator, serviceRef)` resolve to exactly one **operator-signed** listing (the existing discovery verification: schema, re-derived `serviceRef`, recovered EIP-712 operator signature), and the rate is inside its signed `commercial` bounds | `metrik_opener_listing_*` |
|
|
109
|
+
| `deployAndSettle` simulates cleanly from the relayer | `metrik_opener_settle_simulation_failed` |
|
|
110
|
+
|
|
111
|
+
`settle` re-runs all of it, broadcasts `deployAndSettle`, waits for the
|
|
112
|
+
receipt, and reports `success: true` only when the mined receipt contains both
|
|
113
|
+
the escrow's `StreamOpened` (buyer = opener, exactly these terms and deposit)
|
|
114
|
+
and the opener's `StreamOpenedFor` (this payer, nonce and amount). The
|
|
115
|
+
`SettleResponse` carries `extra.metrikStreamOpener = {streamId, opener, escrow,
|
|
116
|
+
deposit, ratePerSecond, expiresAt}`. A revert (for example an authorization that
|
|
117
|
+
was already used) is `success: false` with the reason. A receipt that does not
|
|
118
|
+
arrive in time is `settlement_pending`; a later settle of the same payload
|
|
119
|
+
reconciles against that transaction instead of broadcasting a second one.
|
|
120
|
+
|
|
121
|
+
### At most one success per authorization
|
|
122
|
+
|
|
123
|
+
Each EIP-3009 authorization — keyed by chain, asset, payer and authorization
|
|
124
|
+
nonce — receives **at most one successful `SettleResponse` from a facilitator
|
|
125
|
+
instance, ever**:
|
|
126
|
+
|
|
127
|
+
- A settle (or verify) of an authorization that is already in flight or already
|
|
128
|
+
settled returns `success: false` / `isValid: false` with
|
|
129
|
+
`duplicate_settlement` (carrying the tx hash when there is one). One payment
|
|
130
|
+
can never buy two served responses.
|
|
131
|
+
- After `settlement_pending`, a retry of the SAME payload verifies (even though
|
|
132
|
+
the authorization may already be used on-chain) and its settle reconciles to
|
|
133
|
+
the mined outcome; it receives the one success only if none was delivered
|
|
134
|
+
before.
|
|
135
|
+
- The gateway enforces the same rule on its side: a `PAYMENT-SIGNATURE` whose
|
|
136
|
+
authorization is in flight or already settled gets `409
|
|
137
|
+
x402-payment-already-used` **without** an upstream call; a retry after
|
|
138
|
+
`settlement_pending` is let through so it can reconcile.
|
|
139
|
+
- Relayer submissions (simulate + broadcast) are serialized, so concurrent
|
|
140
|
+
payers never collide on the relayer's account nonce.
|
|
141
|
+
|
|
142
|
+
**Scope of the guarantee: both ledgers are in-memory and per instance.** They
|
|
143
|
+
do not survive a restart and are not shared between replicas. Run exactly one
|
|
144
|
+
facilitator instance (and one gateway instance per listing); a multi-instance
|
|
145
|
+
deployment would need a shared store, which does not exist today. Entries are
|
|
146
|
+
kept until the authorization's `validBefore` has passed (plus a grace), after
|
|
147
|
+
which USDC itself refuses the authorization.
|
|
148
|
+
|
|
149
|
+
## Getting unearned money back: the gasless refund relay
|
|
150
|
+
|
|
151
|
+
**There is no automatic refund.** Nothing pushes money back to a payer, and no
|
|
152
|
+
keeper runs in the background. Unearned money comes back only when someone
|
|
153
|
+
calls the relay endpoints below (or calls the opener directly). What the relay
|
|
154
|
+
removes is the need for ETH: a typical x402 payer holds only USDC, so the
|
|
155
|
+
facilitator's relayer pays the gas for the two transactions a refund needs.
|
|
156
|
+
|
|
157
|
+
The escrow's buyer of record is the opener, so a refund goes through it:
|
|
158
|
+
|
|
159
|
+
1. **Close** (optional — only to stop before `expiresAt`). `opener.close` is
|
|
160
|
+
payer-only on-chain, because anyone else could cut a payer's service. The
|
|
161
|
+
relay submits `opener.closeWithSig` with the payer's EIP-712 signature.
|
|
162
|
+
2. **Reclaim**, once the settlement window allows it: a checkpoint covering the
|
|
163
|
+
stop time (close, or `expiresAt`) is posted — the oracle posts every
|
|
164
|
+
~15–22 s — or `escapeGraceSeconds` (900 s on the live escrow) have passed
|
|
165
|
+
since the stop. `opener.reclaim(streamId, cumulativeAmount, proof)` is
|
|
166
|
+
permissionless: the escrow fixes the amount (a Merkle proof against its
|
|
167
|
+
**latest** checkpoint root) and the opener fixes the destination
|
|
168
|
+
(`payerOf(streamId)`). The relay fetches the proof from the oracle itself.
|
|
169
|
+
3. **Escape hatch**, only if no usable proof ever appears: after
|
|
170
|
+
`expiresAt + escapeGraceSeconds` the payer may sign `ReclaimUnverified` and
|
|
171
|
+
the relay submits `reclaimUnverifiedWithSig`. It returns `deposit -
|
|
172
|
+
claimedCumulative` and forfeits the seller's earned-but-unclaimed revenue, so
|
|
173
|
+
it is **never** relayed without the payer's signature; the unsigned
|
|
174
|
+
`reclaimUnverified` is payer-only on-chain and the relay never calls it.
|
|
175
|
+
|
|
176
|
+
The streamId and the refund URL arrive with the paid response:
|
|
177
|
+
`PAYMENT-RESPONSE.extra.metrikStreamOpener = {streamId, opener, escrow,
|
|
178
|
+
deposit, ratePerSecond, expiresAt, refundUrl}`.
|
|
179
|
+
|
|
180
|
+
### Endpoints (on the facilitator service; public, no bearer token)
|
|
181
|
+
|
|
182
|
+
| Method + path | Body | Does |
|
|
183
|
+
| -------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------- |
|
|
184
|
+
| `GET /streams/:streamId` | — | Status, and exactly what the payer can do next |
|
|
185
|
+
| `POST /streams/:streamId/close` | `{"deadline": "<unix s>", "signature": "0x…"}` | Relays `closeWithSig` (payer's EIP-712 `Close`) |
|
|
186
|
+
| `POST /streams/:streamId/reclaim` | none | Fetches the oracle proof, relays `reclaim` |
|
|
187
|
+
| `POST /streams/:streamId/reclaim-unverified` | `{"deadline": "<unix s>", "signature": "0x…"}` | Relays `reclaimUnverifiedWithSig` (payer's `ReclaimUnverified`) |
|
|
188
|
+
|
|
189
|
+
The payer signs, with the opener as the EIP-712 domain
|
|
190
|
+
(`{name: "Metrik X402StreamOpener", version: "1", chainId, verifyingContract:
|
|
191
|
+
<opener>}`):
|
|
71
192
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
193
|
+
```
|
|
194
|
+
Close(bytes32 streamId,uint256 nonce,uint256 deadline)
|
|
195
|
+
ReclaimUnverified(bytes32 streamId,uint256 nonce,uint256 deadline)
|
|
196
|
+
```
|
|
75
197
|
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
|
|
198
|
+
`nonce` is the payer's current `opener.nonces(payer)`; `deadline` must be at
|
|
199
|
+
least 60 s after the current chain time. `GET /streams/:streamId` returns the
|
|
200
|
+
ready-to-sign typed data (with the current nonce) under `next.typedData`, so a
|
|
201
|
+
client signs it as-is after choosing a deadline. An optional `"nonce"` in the
|
|
202
|
+
body turns a stale nonce into a clear `409 nonce-mismatch`.
|
|
79
203
|
|
|
80
|
-
|
|
81
|
-
with **no** capability gets a real x402 `402` instead of a `403`:
|
|
204
|
+
`GET /streams/:streamId` answers `200` with:
|
|
82
205
|
|
|
83
206
|
```json
|
|
84
207
|
{
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
208
|
+
"streamId": "0x…",
|
|
209
|
+
"chainId": 84532,
|
|
210
|
+
"escrow": "0x…",
|
|
211
|
+
"factory": "0x…",
|
|
212
|
+
"opener": "0x…",
|
|
213
|
+
"payer": "0x…",
|
|
214
|
+
"operator": "0x…",
|
|
215
|
+
"serviceRef": "0x…",
|
|
216
|
+
"deposit": "60000",
|
|
217
|
+
"ratePerSecond": "1000",
|
|
218
|
+
"claimedCumulative": "0",
|
|
219
|
+
"openedAt": 1790452888,
|
|
220
|
+
"expiresAt": 1790452948,
|
|
221
|
+
"closedAt": 1790452902,
|
|
222
|
+
"status": "closed",
|
|
223
|
+
"reclaimed": false,
|
|
224
|
+
"chainTime": 1790452910,
|
|
225
|
+
"settlement": {
|
|
226
|
+
"stopAt": 1790452902,
|
|
227
|
+
"latestCheckpointCheckedAt": 1790452904,
|
|
228
|
+
"checkpointCoversStop": true,
|
|
229
|
+
"fallbackAt": 1790453802,
|
|
230
|
+
"escapeHatchAt": 1790453848
|
|
231
|
+
},
|
|
232
|
+
"oracle": {
|
|
233
|
+
"cumulativeAmount": "14000",
|
|
234
|
+
"checkpointId": "61222",
|
|
235
|
+
"checkedAt": 1790452904,
|
|
236
|
+
"root": "0x…"
|
|
237
|
+
},
|
|
238
|
+
"reclaimable": "46000",
|
|
239
|
+
"payerNonce": "1",
|
|
240
|
+
"next": {
|
|
241
|
+
"action": "reclaim",
|
|
242
|
+
"method": "POST",
|
|
243
|
+
"path": "/streams/0x…/reclaim",
|
|
244
|
+
"message": "…"
|
|
245
|
+
},
|
|
246
|
+
"refunds": "There is no automatic refund. …"
|
|
99
247
|
}
|
|
100
248
|
```
|
|
101
249
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
through one path while they believe they paid through the other.
|
|
108
|
-
2. **`payTo` is derived from the operator-signed descriptor**, resolved at
|
|
109
|
-
startup from the registry for this gateway's `serviceRef`. There is no
|
|
110
|
-
`payTo` environment variable, and the gateway **refuses to start** if the
|
|
111
|
-
signed listing cannot be resolved. `payTo` is the one field whose corruption
|
|
112
|
-
silently redirects money, so it is never configurable and never caller-supplied.
|
|
113
|
-
3. **The price is the seller's and is not signed.** The descriptor schema has
|
|
114
|
-
`minRatePerSecond`/`maxRatePerSecond` for metered streams and no per-call
|
|
115
|
-
price field, so a per-call amount can only come from the seller running the
|
|
116
|
-
gateway. Legitimate — it is their price on their gateway — but a buyer
|
|
117
|
-
**cannot** cross-check it against the registry the way they can cross-check
|
|
118
|
-
the payee.
|
|
119
|
-
|
|
120
|
-
Ordering on a paid request is `verify -> call upstream -> settle -> flush`.
|
|
121
|
-
Settling before the response is flushed means a call that produced nothing is
|
|
122
|
-
never charged for, and a payment that cannot be settled returns `402` rather
|
|
123
|
-
than handing over the goods for free. Both edges favour the buyer.
|
|
124
|
-
|
|
125
|
-
That ordering has one sharp edge, and it is closed rather than merely noted.
|
|
126
|
-
Because the upstream runs _before_ settlement, an upstream slower than the
|
|
127
|
-
signed authorization's own `validBefore` would produce its **side effects** and
|
|
128
|
-
then be unsettleable — the caller never gets the response body, which is flushed
|
|
129
|
-
only after settlement, but the work would have happened for free. The `exact`
|
|
130
|
-
verifier needs only `validBefore >= now + 6s` while the gateway's upstream
|
|
131
|
-
timeout defaults to 30 s, so the two can genuinely cross. The gateway therefore
|
|
132
|
-
caps each upstream call at `min(upstreamTimeout, validBefore - now - 10s)` and
|
|
133
|
-
returns `402` outright when that leaves no usable window. A payment that cannot
|
|
134
|
-
survive its own round trip is refused before the upstream is touched.
|
|
135
|
-
|
|
136
|
-
It also refuses before the upstream when the facilitator reports
|
|
137
|
-
`metrik.settleable: false` — see below.
|
|
138
|
-
|
|
139
|
-
`extra.metrikServiceRef` is published so a client can look the listing up. **The
|
|
140
|
-
facilitator ignores it**, deliberately — it arrives inside a caller-supplied
|
|
141
|
-
body, and reading it would let a caller choose which service's delivery record
|
|
142
|
-
gets checked. There is a test for exactly that.
|
|
143
|
-
|
|
144
|
-
---
|
|
250
|
+
`closedAt` is `null` while active; `oracle` and `reclaimable` are `null` when
|
|
251
|
+
no proof that matches the escrow's on-chain latest root is available (with an
|
|
252
|
+
`oracleNote`). `next.action` is one of `close` (with `typedData` and the body
|
|
253
|
+
shape), `wait` (with `retryAt` / `retryAfterSeconds`), `reclaim`,
|
|
254
|
+
`reclaim-unverified` (with `typedData`), or `none`.
|
|
145
255
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
`POST /verify` does what any facilitator does — delegated **verbatim** to the
|
|
149
|
-
real `x402/facilitator` `verify()`, so Metrik does not reimplement EIP-3009 —
|
|
150
|
-
and then:
|
|
151
|
-
|
|
152
|
-
1. Resolve `PaymentRequirements.resource` to a Metrik listing, **by origin,
|
|
153
|
-
against Metrik's own verified registry view**. Not a Metrik listing? Return
|
|
154
|
-
the ordinary answer and apply no gate.
|
|
155
|
-
2. Check `payTo` equals the operator who **signed** that listing. If not:
|
|
156
|
-
`isValid: false, invalidReason: "invalid_exact_evm_payload_recipient_mismatch"`.
|
|
157
|
-
This is the one check here that protects the payer: without it, anyone could
|
|
158
|
-
quote a Metrik-listed resource and redirect its payment to their own address.
|
|
159
|
-
3. Ask the oracle for that `serviceRef`'s current delivery verdict.
|
|
160
|
-
Fresh failure -> `isValid: false`.
|
|
161
|
-
|
|
162
|
-
`POST /settle` re-runs the entire gate before broadcasting. A facilitator must
|
|
163
|
-
never settle something it would not verify, and `/settle` is reachable without
|
|
164
|
-
`/verify` ever having been called.
|
|
165
|
-
|
|
166
|
-
### `/settle` spends the operator's money, and is fenced accordingly
|
|
167
|
-
|
|
168
|
-
This is the part a security review caught, and it is worth stating plainly
|
|
169
|
-
because the shape of the hole is not obvious.
|
|
170
|
-
|
|
171
|
-
`PaymentRequirementsSchema` accepts **any** `asset`, **any** `payTo`,
|
|
172
|
-
`maxAmountRequired: "0"`, and any valid-URL `resource`. The library's `verify`
|
|
173
|
-
only checks the payload against those same caller-supplied requirements: the
|
|
174
|
-
signature recovers to `authorization.from`, `to === payTo`, the window is valid,
|
|
175
|
-
and `balanceOf(from) >= maxAmountRequired` **on the caller-supplied asset**. An
|
|
176
|
-
attacker signing from their own key to their own address satisfies every one of
|
|
177
|
-
those checks. `settle` then does
|
|
178
|
-
`writeContract({ address: paymentRequirements.asset, ... })`.
|
|
179
|
-
|
|
180
|
-
So an unfenced `/settle` bound to `0.0.0.0` is a permissionless _"make the
|
|
181
|
-
relayer sign a call to a contract I chose, and pay the gas"_ endpoint. Deploy
|
|
182
|
-
something whose `balanceOf` returns a huge number and whose
|
|
183
|
-
`transferWithAuthorization` burns gas in a loop, sign an authorization over its
|
|
184
|
-
domain from your own key to your own address, and POST it until the relayer is
|
|
185
|
-
dry — at which point legitimate settlement stops too. No funds are stolen, but
|
|
186
|
-
it is the operator's money and it induces the operator's key to sign
|
|
187
|
-
transactions to contracts they never chose.
|
|
188
|
-
|
|
189
|
-
Four controls, all enforced before the settler is reached:
|
|
190
|
-
|
|
191
|
-
| Control | Env | Why |
|
|
192
|
-
| ----------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
193
|
-
| **Asset allowlist** | `METRIK_X402_SETTLEMENT_ASSETS` (default: the chain's USDC) | **The one that removes the arbitrary-contract-call primitive.** The relayer will only ever be asked to call an approved token. |
|
|
194
|
-
| **Bearer token on `/settle`** | `METRIK_X402_FACILITATOR_TOKEN` (>= 32 chars) | `/settle` is not a public read. Constant-time compare; a missing configured token DENIES. `/verify` and `/supported` stay public. |
|
|
195
|
-
| **Settlement scope** | `METRIK_X402_SETTLEMENT_SCOPE` (default `tracked-resources`) | The relayer pays gas only for resources that resolve to exactly one signed Metrik listing. |
|
|
196
|
-
| **Network match + non-zero amount** | — | A zero-value settlement moves nothing and only burns gas; a foreign network is not ours to settle. |
|
|
197
|
-
|
|
198
|
-
A settling facilitator **refuses to start** without a token, and the facilitator
|
|
199
|
-
object refuses to be constructed with an empty asset allowlist. Both are startup
|
|
200
|
-
failures rather than request-time ones, so a misconfiguration is loud.
|
|
201
|
-
|
|
202
|
-
**The scope default is deliberately asymmetric with the delivery gate, and the
|
|
203
|
-
asymmetry is the point.** An OPINION about a service costs nothing, so `/verify`
|
|
204
|
-
stays universal and fails open on absent evidence. BROADCASTING costs the
|
|
205
|
-
operator real gas, so `/settle` fails closed — including when the registry
|
|
206
|
-
cannot be read, where `/verify` still answers and `/settle` does not. This is
|
|
207
|
-
not a censorship lever: Metrik declining to spend _its own_ gas does not stop
|
|
208
|
-
anyone being paid, because any other facilitator (or the payer's own relayer)
|
|
209
|
-
can settle the same payment. Operators who want a general-purpose facilitator on
|
|
210
|
-
a relayer they are content to fund for anyone's traffic set
|
|
211
|
-
`METRIK_X402_SETTLEMENT_SCOPE=any-resource`; the asset allowlist still applies.
|
|
212
|
-
|
|
213
|
-
`/verify` reports `metrik.settleable` so a resource server learns **before it
|
|
214
|
-
does any work** whether this relayer would actually broadcast, instead of
|
|
215
|
-
executing the request and then discovering it cannot be paid for.
|
|
216
|
-
|
|
217
|
-
### Origin resolution, and origin squatting
|
|
218
|
-
|
|
219
|
-
A listing owns an ORIGIN (scheme + host + port); matching on origin means a
|
|
220
|
-
seller does not have to re-register every route. An origin claimed by **two
|
|
221
|
-
different services is unresolvable**, not first-come-first-served. Picking the
|
|
222
|
-
first claimant made the outcome depend on the registry's read order, so a second
|
|
223
|
-
signed listing claiming a victim's origin could take over the victim's gate and
|
|
224
|
-
permanently refuse their payments. That is not exploitable while listing
|
|
225
|
-
insertion requires an operator-held key — but marking the origin unresolvable
|
|
226
|
-
costs nothing and removes the primitive before registration ever opens. A single
|
|
227
|
-
listing naming the same origin through several fields (`publicUrl`,
|
|
228
|
-
`interface.baseUrl`, `callerAuth.accessUrl`) is not a conflict.
|
|
229
|
-
|
|
230
|
-
### Only gate what Metrik actually tracks
|
|
231
|
-
|
|
232
|
-
A resource that maps to no Metrik listing gets the ordinary facilitator answer,
|
|
233
|
-
unchanged. Refusing payments for services Metrik has no opinion about would not
|
|
234
|
-
be caution — it would be a broken facilitator.
|
|
235
|
-
|
|
236
|
-
### Which `invalidReason`
|
|
237
|
-
|
|
238
|
-
`invalidReason` is a **closed enum**. There is no member for "this service is not
|
|
239
|
-
delivering", and inventing one breaks every conformant client that parses the
|
|
240
|
-
field. The choice is between imperfect members:
|
|
241
|
-
|
|
242
|
-
| Candidate | Why not |
|
|
243
|
-
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
244
|
-
| `insufficient_funds`, `invalid_payload`, `invalid_exact_evm_payload_signature` | Active lies. They send a developer to debug a wallet that is working correctly. |
|
|
245
|
-
| `unexpected_verify_error` | Also false. This refusal is deliberate and expected, not an error. |
|
|
246
|
-
| **`invalid_payment_requirements`** | **Chosen.** The only member pointing at the _requirements_ side of the exchange — which is exactly where the problem is: Metrik will not settle a payment quoted for _this resource_ to _this payee_ right now. |
|
|
247
|
-
|
|
248
|
-
The real explanation travels in a `metrik` object alongside the standard fields:
|
|
256
|
+
A successful relayed transaction answers `200` only from its **mined** receipt:
|
|
249
257
|
|
|
250
258
|
```json
|
|
251
|
-
{
|
|
252
|
-
"
|
|
253
|
-
|
|
254
|
-
"payer": "0x
|
|
255
|
-
"metrik": {
|
|
256
|
-
"gate": "refused",
|
|
257
|
-
"serviceRef": "0x...",
|
|
258
|
-
"deliveryVerdict": "failing",
|
|
259
|
-
"deliveryReason": "fresh-failing",
|
|
260
|
-
"verdictAgeSeconds": 18,
|
|
261
|
-
"freshnessWindowSeconds": 90,
|
|
262
|
-
"detail": "Metrik's oracle reports this service as FAILING its delivery checks ..."
|
|
263
|
-
}
|
|
264
|
-
}
|
|
259
|
+
{ "ok": true, "action": "close", "streamId": "0x…", "opener": "0x…",
|
|
260
|
+
"payer": "0x…", "transaction": "0x…", "message": "…", "next": { "path": "/streams/0x…/reclaim" } }
|
|
261
|
+
{ "ok": true, "action": "reclaim", "streamId": "0x…", "opener": "0x…",
|
|
262
|
+
"payer": "0x…", "amount": "46000", "entitlement": "14000", "transaction": "0x…", "message": "…" }
|
|
265
263
|
```
|
|
266
264
|
|
|
267
|
-
`
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
Set `METRIK_X402_INDETERMINATE_POLICY=deny` to require positive fresh evidence
|
|
329
|
-
instead. That is a real, supported, tested choice — it is just not a safe
|
|
330
|
-
default.
|
|
331
|
-
|
|
332
|
-
---
|
|
333
|
-
|
|
334
|
-
## Running it
|
|
265
|
+
(`action` is `reclaim-unverified` for the escape hatch.) Every refusal is
|
|
266
|
+
`{"ok": false, "error": "<code>", "message": "…", …}`, with `Retry-After`
|
|
267
|
+
where a retry makes sense:
|
|
268
|
+
|
|
269
|
+
| Status | `error` | Meaning |
|
|
270
|
+
| ------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
271
|
+
| 400 | `invalid-stream-id`, `invalid-request`, `invalid-json` | Malformed input |
|
|
272
|
+
| 403 | `invalid-signature`, `signature-expired` | Not the payer's EIP-712 signature for this opener, stream and nonce; or deadline too near |
|
|
273
|
+
| 404 | `not-found` | Unknown stream, or its buyer is not an opener of the configured factory |
|
|
274
|
+
| 409 | `already-closed`, `stream-expired`, `already-reclaimed`, `nothing-to-reclaim`, `nonce-mismatch` | Would have no effect; nothing sent |
|
|
275
|
+
| 409 | `not-reclaimable-yet`, `settlement-window-open`, `proof-unavailable`, `escape-hatch-not-ready` | Not yet; `retryAt` (unix s) says when at the latest |
|
|
276
|
+
| 409 | `simulation-failed`, `in-progress`, `blocked` | The simulation reverted; a relayed tx is in flight; an earlier tx had an unexpected outcome |
|
|
277
|
+
| 202 | `pending` | Sent, not mined yet (`transaction`); repeat the same request — it is never re-sent |
|
|
278
|
+
| 429 | `rate-limited` | Per-IP, per-stream or global budget exhausted |
|
|
279
|
+
| 502 | `transaction-reverted`, `unexpected-receipt`, `submission-failed` | Never a success without the expected mined receipt |
|
|
280
|
+
| 503 | `chain-unavailable`, `oracle-unavailable`, `opener-domain-mismatch` | Fail closed |
|
|
281
|
+
|
|
282
|
+
### Bounded against gas abuse
|
|
283
|
+
|
|
284
|
+
- **Opener streams only.** `factory.isOpener(stream.buyer)` is read on-chain
|
|
285
|
+
first; every other stream is `404` before any signature check, oracle call or
|
|
286
|
+
simulation.
|
|
287
|
+
- **No effect, no transaction.** Every precondition the contract checks is read
|
|
288
|
+
first (status, expiry, settlement window, `reclaimed`, a non-zero refund). A
|
|
289
|
+
reclaim's simulation must return exactly the non-zero `reclaimableFor`
|
|
290
|
+
amount. Nothing is broadcast without a successful simulation.
|
|
291
|
+
- **Proofs are checked, not trusted.** The oracle's proof must be for this
|
|
292
|
+
stream, verify under the escrow's leaf
|
|
293
|
+
`keccak256(bytes.concat(keccak256(abi.encode(streamId, cumulative))))`, and
|
|
294
|
+
its root must equal the escrow's on-chain `latestCheckpointRoot`. A tampered
|
|
295
|
+
proof (for example one claiming less delivery) is refused without a
|
|
296
|
+
transaction; the contract would refuse it anyway.
|
|
297
|
+
- **At most one successful close and one successful reclaim per stream** from
|
|
298
|
+
a facilitator instance (the proof and escape-hatch paths share the reclaim
|
|
299
|
+
slot, as the escrow allows one). Concurrent duplicates are refused
|
|
300
|
+
synchronously; a repeat after success is refused without any transaction.
|
|
301
|
+
- **Read fan-out is bounded.** Every `/streams` request (read or write) counts
|
|
302
|
+
against a per-client-IP limit AND an IP-independent global limit (default
|
|
303
|
+
120/min, `429` + `Retry-After`). A stream's status and its oracle proof are
|
|
304
|
+
reused for a few seconds (default 5 s), so repeated requests for one stream
|
|
305
|
+
do not re-hit the RPC or the oracle; the cache for a stream is dropped as
|
|
306
|
+
soon as a relayed transaction for it is mined, and a broadcast is still
|
|
307
|
+
decided by a fresh simulation.
|
|
308
|
+
- **No internal errors leak.** RPC, oracle and simulation failures are
|
|
309
|
+
answered with a generic message and an error code (`chain-unavailable`,
|
|
310
|
+
`oracle-unavailable`, `simulation-failed`, `submission-failed`); the detail
|
|
311
|
+
is logged server-side with URL paths redacted, never the relayer key. Only
|
|
312
|
+
a decoded contract custom-error name (for example `StreamClosedAlready`) is
|
|
313
|
+
returned.
|
|
314
|
+
- **Budgets.** Per client IP (all `/streams` routes), per stream (relayed
|
|
315
|
+
submission attempts) and globally per minute. Refused requests — a forged
|
|
316
|
+
signature, a request with no effect — do not consume a stream's budget, so
|
|
317
|
+
spamming cannot lock a payer out.
|
|
318
|
+
- **Receipts.** Success only from a mined receipt with the escrow's
|
|
319
|
+
`StreamClosed` / `BuyerReclaimed` and the opener's `StreamCloseRequested` /
|
|
320
|
+
`ReclaimForwarded` for exactly this stream, payer and amount.
|
|
321
|
+
- **One relayer queue.** Relay submissions and `deployAndSettle` go through the
|
|
322
|
+
same serialized queue, so they never race for the relayer's nonce.
|
|
323
|
+
|
|
324
|
+
## Running the facilitator
|
|
335
325
|
|
|
336
326
|
```bash
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
# A seller's gateway, quoting 0.001 USDC per call through it
|
|
343
|
-
METRIK_GATEWAY_X402_PRICE_BASE_UNITS=1000 \
|
|
344
|
-
METRIK_GATEWAY_X402_FACILITATOR_URL=https://facilitator.example \
|
|
345
|
-
METRIK_GATEWAY_X402_RESOURCE_BASE_URL=https://svc.example \
|
|
346
|
-
npx metrik-gateway
|
|
327
|
+
METRIK_OPENER_FACILITATOR_ENABLED=1 \
|
|
328
|
+
METRIK_OPENER_FACILITATOR_RPC_URL=https://sepolia.base.org \
|
|
329
|
+
METRIK_OPENER_FACILITATOR_RELAYER_PRIVATE_KEY=<from your secret store> \
|
|
330
|
+
METRIK_OPENER_FACILITATOR_AUTH_TOKEN=<at least 32 random characters> \
|
|
331
|
+
npx metrik-x402-opener-facilitator
|
|
347
332
|
```
|
|
348
333
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
`
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
334
|
+
| Variable | Default | Meaning |
|
|
335
|
+
| -------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
336
|
+
| `METRIK_OPENER_FACILITATOR_ENABLED` | off | Must be truthy or the service refuses to start. |
|
|
337
|
+
| `METRIK_OPENER_FACILITATOR_RPC_URL` | required | RPC for reads, simulation and the settlement transaction. |
|
|
338
|
+
| `METRIK_OPENER_FACILITATOR_RELAYER_PRIVATE_KEY` | required | Pays gas for `deployAndSettle`. Never holds buyer funds; never logged. |
|
|
339
|
+
| `METRIK_OPENER_FACILITATOR_AUTH_TOKEN` | required (≥ 32 chars) | Bearer token required on `/settle`. |
|
|
340
|
+
| `METRIK_OPENER_FACILITATOR_FACTORY_ADDRESS` | the Base Sepolia factory | Required on any other chain. |
|
|
341
|
+
| `METRIK_OPENER_FACILITATOR_ESCROW_ADDRESS` | live Base Sepolia `StreamEscrowV2` | Must equal `factory.escrow()`. |
|
|
342
|
+
| `METRIK_OPENER_FACILITATOR_NETWORK` | `eip155:84532` | Must match the RPC's chain id. |
|
|
343
|
+
| `METRIK_OPENER_FACILITATOR_ASSETS` | Circle test USDC | Comma-separated allowlist; must contain `factory.usdc()`. |
|
|
344
|
+
| `METRIK_OPENER_FACILITATOR_REGISTRY_URL` | `https://oracle.metrik.live/listings` | Where operator-signed listings are read. |
|
|
345
|
+
| `METRIK_OPENER_FACILITATOR_LISTING_CACHE_SECONDS` | `60` | Reuse of a successful registry read (failures are never cached). |
|
|
346
|
+
| `METRIK_OPENER_FACILITATOR_RECEIPT_TIMEOUT_SECONDS` | `60` | Receipt wait before `settlement_pending`. |
|
|
347
|
+
| `METRIK_OPENER_FACILITATOR_REQUIRE_AUTH_ON_VERIFY` | **on** | Require the token on `/verify` too; `0` opts out explicitly. |
|
|
348
|
+
| `METRIK_OPENER_FACILITATOR_MIN_AMOUNT` | `10000` (0.01 USDC) | Smallest accepted payment; advertised in the marker. |
|
|
349
|
+
| `METRIK_OPENER_FACILITATOR_SETTLE_RATE_LIMIT_PER_MINUTE` | `60` | Authorized `/settle` calls per token per minute; beyond it `429`. |
|
|
350
|
+
| `METRIK_OPENER_FACILITATOR_HOST` / `_PORT` (or `PORT`) | `127.0.0.1` / `8788` | Bind address. Put TLS in front of it. |
|
|
351
|
+
| `METRIK_OPENER_FACILITATOR_REFUND_RELAY` | **on** | The gasless refund relay; `0` switches it off (a gateway then refuses to start). |
|
|
352
|
+
| `METRIK_OPENER_FACILITATOR_ORACLE_URL` | `https://oracle.metrik.live` | Serves `GET /checkpoint/proof?streamId=`. Required off Base Sepolia. |
|
|
353
|
+
| `METRIK_OPENER_FACILITATOR_PUBLIC_URL` | unset | Public origin, for absolute links (`next.url`) in relay responses. |
|
|
354
|
+
| `METRIK_OPENER_FACILITATOR_REFUND_RATE_LIMIT_PER_IP_PER_MINUTE` | `30` | `/streams/*` requests per client IP per minute. |
|
|
355
|
+
| `METRIK_OPENER_FACILITATOR_REFUND_GLOBAL_RATE_LIMIT_PER_MINUTE` | `120` | `/streams/*` requests per minute from all clients together (IP-independent). |
|
|
356
|
+
| `METRIK_OPENER_FACILITATOR_REFUND_READ_CACHE_SECONDS` | `5` | Reuse of one stream's status and oracle proof; `0` disables. |
|
|
357
|
+
| `METRIK_OPENER_FACILITATOR_REFUND_SUBMISSIONS_PER_STREAM_PER_MINUTE` | `3` | Relayed transactions per stream per minute. |
|
|
358
|
+
| `METRIK_OPENER_FACILITATOR_REFUND_SUBMISSIONS_PER_MINUTE` | `30` | Relayed refund transactions per minute, all streams. |
|
|
359
|
+
| `METRIK_OPENER_FACILITATOR_TRUST_PROXY` | off | Key the per-IP limit on the right-most `X-Forwarded-For` hop. See the warning below. |
|
|
360
|
+
|
|
361
|
+
> **`TRUST_PROXY=1` is only safe when the service is bound to loopback
|
|
362
|
+
> (`METRIK_OPENER_FACILITATOR_HOST=127.0.0.1`, the default) behind a reverse
|
|
363
|
+
> proxy that overwrites `X-Forwarded-For` with the address it actually saw
|
|
364
|
+
> (Caddy's `reverse_proxy` does).** Never enable it when the port is reachable
|
|
365
|
+
> directly: any client could then send its own `X-Forwarded-For` and get a
|
|
366
|
+
> fresh per-IP budget on every request. The global cap still applies either
|
|
367
|
+
> way.
|
|
368
|
+
|
|
369
|
+
At startup it proves the wiring (RPC chain id, factory code, `factory.escrow()`,
|
|
370
|
+
`factory.usdc()` allowlisted; for the relay, `factory.escrow()` and the escrow's
|
|
371
|
+
`escapeGraceSeconds`) and refuses to start otherwise. Endpoints:
|
|
372
|
+
`GET /supported`, `POST /verify`, `POST /settle` (bearer), `GET /health`, and
|
|
373
|
+
the refund relay's `GET /streams/:streamId` and
|
|
374
|
+
`POST /streams/:streamId/{close,reclaim,reclaim-unverified}` (public).
|
|
375
|
+
|
|
376
|
+
## Running the gateway in x402 mode
|
|
377
|
+
|
|
378
|
+
The gateway's normal variables (`METRIK_GATEWAY_ESCROW_ADDRESS`,
|
|
379
|
+
`METRIK_GATEWAY_RPC_URL`, `METRIK_GATEWAY_SERVICE_REF`,
|
|
380
|
+
`METRIK_GATEWAY_UPSTREAM_URL`, …) are unchanged. The listing is the primary
|
|
381
|
+
`METRIK_GATEWAY_SERVICE_REF`. Add:
|
|
382
|
+
|
|
383
|
+
| Variable | Default | Meaning |
|
|
384
|
+
| ---------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
385
|
+
| `METRIK_GATEWAY_X402V2_OPENER` | off | Truthy to enable the mode. |
|
|
386
|
+
| `METRIK_GATEWAY_X402V2_FACILITATOR_URL` | required | The Metrik opener facilitator. |
|
|
387
|
+
| `METRIK_GATEWAY_X402V2_FACILITATOR_TOKEN` | required | Its `/settle` bearer token. |
|
|
388
|
+
| `METRIK_GATEWAY_X402V2_OPERATOR` | required | Must be the operator that signed the listing. |
|
|
389
|
+
| `METRIK_GATEWAY_X402V2_RATE_PER_SECOND` | required | USDC base units per second. |
|
|
390
|
+
| `METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS` | required | Stream lifetime. |
|
|
391
|
+
| `METRIK_GATEWAY_X402V2_PRICE` | required | Base units per paid request (= deposit); `0 < price <= rate × maxDuration`. |
|
|
392
|
+
| `METRIK_GATEWAY_X402V2_FACTORY_ADDRESS` | the Base Sepolia factory | Required on any other chain. |
|
|
393
|
+
| `METRIK_GATEWAY_X402V2_NETWORK` | `eip155:<METRIK_GATEWAY_CHAIN_ID>` | Must match the chain id. |
|
|
394
|
+
| `METRIK_GATEWAY_X402V2_ASSET` / `_ASSET_NAME` / `_ASSET_VERSION` | Circle test USDC / `USDC` / `2` | Token and its EIP-712 domain. |
|
|
395
|
+
| `METRIK_GATEWAY_X402V2_MAX_TIMEOUT_SECONDS` | `300` | Authorization validity the client signs for. |
|
|
396
|
+
| `METRIK_GATEWAY_X402V2_FACILITATOR_TIMEOUT_SECONDS` | `120` | Per-request facilitator timeout; must exceed its receipt wait by 30 s. |
|
|
397
|
+
| `METRIK_GATEWAY_X402V2_RESOURCE_BASE_URL` | derived from `Host` | Public origin used in the 402 `resource.url`. |
|
|
398
|
+
| `METRIK_GATEWAY_X402V2_REGISTRY_URL` | the public registry | Where the listing is proven at startup. |
|
|
399
|
+
| `METRIK_GATEWAY_X402V2_REFUND_BASE_URL` | the facilitator URL | Public base of the refund relay advertised in the 402. If different, its `/supported` must show the same marker with `refundRelay: true`. |
|
|
400
|
+
| `METRIK_GATEWAY_X402V2_DOCS_URL` | this document on GitHub | Docs link in the 402. |
|
|
401
|
+
| `METRIK_GATEWAY_X402V2_ASSET_DECIMALS` | `6` | Decimals for the amounts in the 402 description. |
|
|
402
|
+
| `METRIK_GATEWAY_X402V2_DESCRIPTION` | unset | One sentence on what the service does, prefixed to the fixed disclosure. |
|
|
403
|
+
| `METRIK_GATEWAY_X402V2_SCHEMA` | unset | JSON `{info: {input, output}, schema}`, declared as `extensions.bazaar` (declared only, never registered). |
|
|
404
|
+
|
|
405
|
+
These names deliberately differ from the removed v1 mode's variables, so a
|
|
406
|
+
leftover v1 environment can never switch a payment mode on.
|
|
407
|
+
|
|
408
|
+
At startup the gateway validates the terms and price, proves the listing is
|
|
409
|
+
operator-signed, checks `factory.escrow()` and `factory.usdc()`, computes
|
|
410
|
+
`payTo = factory.predict(terms)`, and checks the facilitator's marker —
|
|
411
|
+
including `refundRelay: true`, because the 402 promises a refund path without
|
|
412
|
+
ETH — plus the marker at a separate refund base URL; any failure stops it from
|
|
413
|
+
starting. The advertised window is `METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS`
|
|
414
|
+
(for example `60`, with `RATE_PER_SECOND=1000` and `PRICE=60000`).
|
|
415
|
+
|
|
416
|
+
### What the 402 says
|
|
417
|
+
|
|
418
|
+
The resource `description` states plainly what one payment buys; an operator
|
|
419
|
+
`DESCRIPTION` can only be prefixed to it. With a 60 s window at 1000 base
|
|
420
|
+
units/s and a price of 60000:
|
|
421
|
+
|
|
422
|
+
> Pays for one call. Your payment of 0.06 USDC funds a Metrik escrow stream of
|
|
423
|
+
> 60 seconds at 0.001 USDC/s; the seller earns only for seconds Metrik's
|
|
424
|
+
> verifier confirms the service is delivering, and anything unearned is
|
|
425
|
+
> refundable to you (no ETH needed) via
|
|
426
|
+
> https://<facilitator>/streams/{streamId}. Refunds are not automatic: close
|
|
427
|
+
> the stream and reclaim there, using the streamId from the PAYMENT-RESPONSE.
|
|
428
|
+
> Docs: <docs URL>
|
|
429
|
+
|
|
430
|
+
When the price is below `rate × window`, it adds "(the seller can earn at most
|
|
431
|
+
<price>)". `extra.metrikStreamOpener` also carries `refundUrl` (the
|
|
432
|
+
template) and `docsUrl`; the facilitator accepts exactly these two
|
|
433
|
+
informational http(s) links on top of the terms, never uses them for
|
|
434
|
+
settlement, and still refuses any other key. "Verified" here means verified
|
|
435
|
+
delivery, not verified output.
|
|
436
|
+
|
|
437
|
+
| Request carries | Result |
|
|
438
|
+
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
439
|
+
| nothing | `402`, `PAYMENT-REQUIRED` quoting the opener `payTo` and the price |
|
|
440
|
+
| `PAYMENT-SIGNATURE` | verified → upstream → settled if upstream `< 400` → `200` + body + `PAYMENT-RESPONSE` |
|
|
441
|
+
| `PAYMENT-SIGNATURE`, upstream `>= 400` | upstream response, nothing settled |
|
|
442
|
+
| `PAYMENT-SIGNATURE`, settle fails | `402` + `PAYMENT-RESPONSE`, no upstream body |
|
|
443
|
+
| `x-metrik-capability` | the capability path, unchanged |
|
|
444
|
+
| capability **and** `PAYMENT-SIGNATURE` | `400 ambiguous-capability-and-payment` |
|
|
445
|
+
| probe capability **and** `PAYMENT-SIGNATURE` | `400 ambiguous-probe-mode` |
|
|
446
|
+
| any request while the facilitator marker cannot be proven | `503 x402-facilitator-unavailable` |
|
|
447
|
+
| a `PAYMENT-SIGNATURE` already in flight or already settled | `409 x402-payment-already-used`, no upstream call |
|
|
448
|
+
|
|
449
|
+
With `METRIK_GATEWAY_X402V2_OPENER` unset the gateway is byte for byte the
|
|
450
|
+
capability-only gateway (`test/gateway-http-baseline.test.ts`), and the
|
|
451
|
+
`@x402/*` packages are never loaded.
|
|
452
|
+
|
|
453
|
+
## Paying as a buyer: `metrik pay`, MCP `x402_pay`, `@absol-labs/agent/x402-buyer`
|
|
454
|
+
|
|
455
|
+
The buyer side is the SDK's `payWithX402` (`@absol-labs/sdk/x402`) with every SDK
|
|
456
|
+
protection intact, wrapped by `payX402WithMandate` (`@absol-labs/agent/x402-buyer`)
|
|
457
|
+
so the package's golden rule holds: **no spend without a mandate**. A Metrik x402
|
|
458
|
+
payment opens a stream, so it is checked as the stream open it is:
|
|
459
|
+
|
|
460
|
+
| Mandate cap | x402 quote field |
|
|
461
|
+
| ---------------------- | --------------------------------------------- |
|
|
462
|
+
| `maxPerStreamUsdc` | `amount` (the deposit, the price of the call) |
|
|
463
|
+
| `maxTotalUsdc` | spent so far + `amount` |
|
|
464
|
+
| `maxRatePerSecondUsdc` | `ratePerSecond` |
|
|
465
|
+
| `maxDurationSeconds` | `maxDuration` |
|
|
466
|
+
| `allowedOperators` | `operator` |
|
|
467
|
+
|
|
468
|
+
The mandate's signature, expiry, revocation, owner (= the payer) and chain are
|
|
469
|
+
verified before the seller is contacted. The per-quote check runs in the SDK's
|
|
470
|
+
`onQuote` hook — after the SDK has proven `payTo == factory.predict(terms)` on
|
|
471
|
+
chain and before anything is signed — and the caps are also passed to the SDK as
|
|
472
|
+
`maxAmount` / `maxRatePerSecond`. A quote without opener terms is refused
|
|
473
|
+
(`X402MandateDeniedError` reason `not-an-opener-quote`).
|
|
401
474
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
475
|
+
```bash
|
|
476
|
+
metrik pay https://x402.137.23.50.249.sslip.io/transform --json '{"text":"# hi"}' --max-amount 0.1
|
|
477
|
+
metrik refund <streamId> --refund-url <url printed above> --close --reclaim
|
|
478
|
+
```
|
|
405
479
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
480
|
+
The MCP server offers the same as `x402_pay`, `refund_status` and `refund`
|
|
481
|
+
(see [mcp-reference.md](./mcp-reference.md)); `x402_pay` shares the server
|
|
482
|
+
mandate and cumulative spend with `hire_verified_service`. Refunds are still
|
|
483
|
+
**requested, never pushed**: `metrik refund` / `refund` call the relay; nothing
|
|
484
|
+
calls it for you.
|
|
485
|
+
|
|
486
|
+
The facilitator's `PAYMENT-RESPONSE` `extra.metrikStreamOpener.expiresAt` is a
|
|
487
|
+
JSON **number** (it was a decimal string before metrik-agent#121, which the SDK's
|
|
488
|
+
strict parser rejects after settlement — a facilitator deployed before that fix
|
|
489
|
+
must be redeployed).
|
|
490
|
+
|
|
491
|
+
## Network configuration
|
|
492
|
+
|
|
493
|
+
Network, USDC (and its EIP-712 domain), escrow, opener factory and oracle all
|
|
494
|
+
default from the `@absol-labs/sdk` network registry entry for the configured
|
|
495
|
+
CAIP-2 network (`METRIK_OPENER_FACILITATOR_NETWORK`, `METRIK_GATEWAY_CHAIN_ID` /
|
|
496
|
+
`METRIK_GATEWAY_X402V2_NETWORK`); every one has an env override, and on a network
|
|
497
|
+
the registry does not know (or one with no opener factory) each must be given
|
|
498
|
+
explicitly. The refund relay computes the no-proof escape hatch per escrow
|
|
499
|
+
generation (`escrow.generation`): V2 `expiresAt + escapeGraceSeconds`, V3
|
|
500
|
+
`min(expiresAt, closedAt) + escapeGraceSeconds` read from the escrow's
|
|
501
|
+
`escapeHatchReadyAt` view.
|
|
502
|
+
|
|
503
|
+
## Programmatic use
|
|
504
|
+
|
|
505
|
+
Everything lives under the `@absol-labs/agent/x402-opener` subpath (the root
|
|
506
|
+
barrel does not load `@x402/*`): `MetrikOpenerExactScheme`,
|
|
507
|
+
`buildOpenerFacilitator`, `createOpenerFacilitatorServer`, `OpenerRefundRelay`,
|
|
508
|
+
`openerActionTypedData`, `checkpointLeaf`, `RelayerQueue`,
|
|
509
|
+
`RegistryListingResolver`, `readOpenerFacilitatorMarker`, the vendored
|
|
510
|
+
`x402StreamOpenerFactoryAbi` / `x402StreamOpenerAbi`, and the terms helpers.
|
|
511
|
+
The gateway mode is `buildGatewayX402OpenerMode` in
|
|
512
|
+
`@absol-labs/agent/gateway/x402-opener-mode`, passed to
|
|
513
|
+
`createCallerAuthGatewayServer({ x402Opener })`.
|
|
514
|
+
|
|
515
|
+
## Honest limits
|
|
516
|
+
|
|
517
|
+
- **Refunds are requested, never pushed.** There is no automatic refund: the
|
|
518
|
+
relay acts only when an endpoint is called, and a payer who never calls it
|
|
519
|
+
(or never has anyone call `reclaim` for them) gets nothing back. `metrik
|
|
520
|
+
refund` and the MCP `refund` tool call it on request; `metrik close` /
|
|
521
|
+
`metrik reclaim` also route an opener stream through the opener directly.
|
|
522
|
+
- **The relay depends on the oracle for proofs.** If the oracle's latest
|
|
523
|
+
checkpoint never includes the stream, the proof-based reclaim cannot run and
|
|
524
|
+
the payer-signed escape hatch (after `expiresAt + escapeGraceSeconds`) is the
|
|
525
|
+
way out. The relay ledgers are in-memory and per instance, like the
|
|
526
|
+
settlement ledger; the on-chain contracts still allow only one close and one
|
|
527
|
+
reclaim per stream regardless.
|
|
528
|
+
- **Stray funds are unrecoverable**: USDC that reaches an opener any way other
|
|
529
|
+
than `deployAndSettle` / `settleAndOpen` stays there (see the hard rule).
|
|
530
|
+
- **Blacklist blast radius**: one opener is the escrow buyer for every payer of
|
|
531
|
+
a listing's terms; if the token issuer blacklists it, refunds through it
|
|
532
|
+
freeze until it is un-blacklisted.
|
|
533
|
+
- **ERC-6492 counterfactual wallets are refused**; deploy the wallet first. A
|
|
534
|
+
deployed ERC-1271 wallet works (USDC verifies it).
|
|
535
|
+
- **Single instance**: one relayer, and the at-most-one-success ledgers are
|
|
536
|
+
in-memory per instance (see above). Do not run replicas.
|
|
537
|
+
- **Listing freshness**: a successful registry read is reused for 60 s by
|
|
538
|
+
default, so a withdrawn listing can keep verifying for up to that long.
|
|
539
|
+
Discovery prefers a gated listing over a public one for the same operator and
|
|
540
|
+
origin; a listing hidden that way does not resolve (fail closed).
|
|
409
541
|
|
|
410
542
|
---
|
|
411
543
|
|
|
412
|
-
##
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
-
|
|
417
|
-
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
and
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
`
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
| `auth-capture` concept | Metrik equivalent |
|
|
459
|
-
| ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- |
|
|
460
|
-
| `authorize` — reserve client funds into a hold | `openStream` — escrow the budget |
|
|
461
|
-
| `capture` — pay held funds out, **repeatable up to the held total** | `claimStream` — claim verified accrual at each checkpoint |
|
|
462
|
-
| `void` — release the remaining hold | `closeStream` |
|
|
463
|
-
| `reclaim` — _client_ recovers its own hold after the capture deadline, never relayed through the facilitator | `reclaimStream`, gated by `escapeGraceSeconds` |
|
|
464
|
-
| `extra.captureDeadline` | stream expiry |
|
|
465
|
-
| "Metered work priced only once it completes: hold the ceiling, capture the actual cost" | the entire Metrik thesis |
|
|
466
|
-
|
|
467
|
-
The scheme's own summary could be describing Metrik. So why not implement it?
|
|
468
|
-
|
|
469
|
-
**Because the EVM binding is pinned to a specific escrow contract that Metrik is
|
|
470
|
-
not.** [`scheme_auth_capture_evm.md`](https://github.com/x402-foundation/x402/blob/main/specs/schemes/auth-capture/scheme_auth_capture_evm.md)
|
|
471
|
-
requires a canonical `AuthCaptureEscrow` singleton — Base's Commerce Payments
|
|
472
|
-
Protocol — at one of two fixed CREATE2 addresses:
|
|
473
|
-
|
|
474
|
-
| Constant | v1.1 | v1.0 |
|
|
475
|
-
| ----------------------------- | -------------------------------------------- | -------------------------------------------- |
|
|
476
|
-
| `AUTH_CAPTURE_ESCROW_ADDRESS` | `0xf96815976523E00e65Be8f34cA5e64b4f41EB19c` | `0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff` |
|
|
477
|
-
|
|
478
|
-
A conformant facilitator must read `AuthCaptureEscrow.paymentState(paymentInfoHash)`,
|
|
479
|
-
and must simulate each operation and assert that **that** escrow emitted
|
|
480
|
-
`PaymentAuthorized` / `PaymentCharged` with the expected payment hash before
|
|
481
|
-
relaying. Metrik settles in `StreamEscrowV2` at
|
|
482
|
-
`0x0f09f36Ccc05A7c9882F438721C08De314dFd46C`, which is a different contract with
|
|
483
|
-
a different state model and no such functions or events.
|
|
484
|
-
|
|
485
|
-
So `auth-capture` conformance is **not a wire-format exercise. It is a contract
|
|
486
|
-
deployment.** Metrik would have to either settle through the canonical
|
|
487
|
-
`AuthCaptureEscrow` (giving up the per-second metering and checkpoint model that
|
|
488
|
-
is the product) or get a Metrik binding specified and adopted. Neither is a code
|
|
489
|
-
change in this package, and the contracts are frozen.
|
|
544
|
+
## History: the removed x402 v1 integration
|
|
545
|
+
|
|
546
|
+
Until metrik-agent#115 this package shipped an x402 **v1** integration: a
|
|
547
|
+
standard `exact` facilitator (`metrik-x402-facilitator`, exported as
|
|
548
|
+
`@absol-labs/agent/x402-facilitator`) that consulted Metrik's oracle before
|
|
549
|
+
settling, and an opt-in mode on the seller gateway that answered an
|
|
550
|
+
uncapabilitied request with an x402 `402`. It was removed, before it ever ran in
|
|
551
|
+
production, for four reasons:
|
|
552
|
+
|
|
553
|
+
1. **It targeted a superseded protocol version.** It was built on `x402@1.2.0`
|
|
554
|
+
(`x402Version: 1`, `X-PAYMENT` / `X-PAYMENT-RESPONSE`, bare network names
|
|
555
|
+
such as `base-sepolia`). The protocol is at v2 (`PAYMENT-REQUIRED` /
|
|
556
|
+
`PAYMENT-SIGNATURE` / `PAYMENT-RESPONSE`, CAIP-2 network identifiers such as
|
|
557
|
+
`eip155:84532`).
|
|
558
|
+
2. **It paid the seller directly.** `payTo` was the operator, so the money never
|
|
559
|
+
touched `StreamEscrowV2`: no escrow, no protocol fee, and nothing a buyer
|
|
560
|
+
could get back.
|
|
561
|
+
3. **Its delivery gate gated nothing in practice.** On absent evidence it
|
|
562
|
+
defaulted to _allow_, and the live oracle's `GET /services` is built from
|
|
563
|
+
active streams, which was empty — so every payment would have passed.
|
|
564
|
+
4. **Leaving it in place invited someone to enable it.** It was default-off
|
|
565
|
+
behind environment switches and had never broadcast a settlement.
|
|
566
|
+
|
|
567
|
+
What changed for integrators:
|
|
568
|
+
|
|
569
|
+
- The `metrik-x402-facilitator` bin, the `./x402-facilitator` subpath export,
|
|
570
|
+
the `x402:facilitator` script and every module behind them are gone.
|
|
571
|
+
- The seller gateway no longer reads any `METRIK_GATEWAY_X402_*` variable, and
|
|
572
|
+
the facilitator's `METRIK_X402_*` variables have no reader. A gateway that
|
|
573
|
+
never set them behaves exactly as before: a request with no capability gets
|
|
574
|
+
`403 {"authorized":false,"reason":"missing-capability-header"}`, and a request
|
|
575
|
+
with a valid capability is proxied unchanged
|
|
576
|
+
(`test/gateway-http-baseline.test.ts` pins this over a real socket).
|
|
577
|
+
- A request carrying an oracle probe capability **and** an `X-PAYMENT` header is
|
|
578
|
+
still refused with `400 ambiguous-probe-mode` (and, in the v2 mode, so is a
|
|
579
|
+
probe carrying `PAYMENT-SIGNATURE`). A probe is free verification
|
|
580
|
+
traffic and is never mixed with anything that looks like a payment.
|
|
581
|
+
- The deprecated `X402*` / `x402*` aliases of the native flow's exports were
|
|
582
|
+
removed at the same time. Use the canonical names listed below.
|
|
583
|
+
|
|
584
|
+
`test/x402-v1-removed.test.ts` fails if any source, test, script or example
|
|
585
|
+
imports the v1 `x402` / `x402-fetch` packages again (the v2 `@x402/*` packages
|
|
586
|
+
are what the opener path uses), or if `package.json` re-declares them or the
|
|
587
|
+
removed bin, script or export. `scripts/check-packed-package.mjs` refuses to
|
|
588
|
+
publish a tarball that still contains any removed artifact.
|
|
490
589
|
|
|
491
590
|
---
|
|
492
591
|
|
|
493
|
-
##
|
|
494
|
-
|
|
495
|
-
**Two separate flows.** Do not confuse them.
|
|
496
|
-
|
|
497
|
-
### 1. Standard x402 `exact`, with a delivery gate — the subject of the top half
|
|
592
|
+
## The Metrik-native verified-stream `402` flow — NOT x402
|
|
498
593
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
| `PaymentRequirements` / `402` body builder | [`src/x402/payment-requirements.ts`](../src/x402/payment-requirements.ts) |
|
|
502
|
-
| Oracle delivery verdict + freshness | [`src/x402/delivery-oracle.ts`](../src/x402/delivery-oracle.ts) |
|
|
503
|
-
| resource URL -> signed listing | [`src/x402/resource-index.ts`](../src/x402/resource-index.ts) |
|
|
504
|
-
| The gate (`/verify`, `/settle`, `/supported`) | [`src/x402/verified-delivery-facilitator.ts`](../src/x402/verified-delivery-facilitator.ts) |
|
|
505
|
-
| Chain wiring (delegates to real `x402/facilitator`) | [`src/x402/relayer.ts`](../src/x402/relayer.ts) |
|
|
506
|
-
| HTTP server + env contract | [`src/x402/facilitator-server.ts`](../src/x402/facilitator-server.ts) |
|
|
507
|
-
| Gateway 402 mode | [`src/gateway/x402-mode.ts`](../src/gateway/x402-mode.ts) |
|
|
508
|
-
|
|
509
|
-
Standard scheme, standard headers, standard client. `x402` is an **optional peer
|
|
510
|
-
dependency**: the facilitator lives behind its own entry point
|
|
511
|
-
(`@absol-labs/agent/x402-facilitator`) and the `metrik-x402-facilitator` bin, so
|
|
512
|
-
a plain `npm install @absol-labs/agent` never pulls it in.
|
|
513
|
-
|
|
514
|
-
### 2. The Metrik-native verified-stream `402` flow — NOT x402
|
|
515
|
-
|
|
516
|
-
In [`src/x402/facilitator.ts`](../src/x402/facilitator.ts):
|
|
594
|
+
In [`src/x402/facilitator.ts`](../src/x402/facilitator.ts) (the directory name is
|
|
595
|
+
historical):
|
|
517
596
|
|
|
518
597
|
- scheme string **`streamproof-verified-stream`** — deliberately _not_ an x402
|
|
519
598
|
scheme name, and not registered with x402;
|
|
@@ -525,124 +604,78 @@ In [`src/x402/facilitator.ts`](../src/x402/facilitator.ts):
|
|
|
525
604
|
plus an owner-signed `SpendMandate` — **not** an x402 `PaymentPayload`;
|
|
526
605
|
- no `/verify`, no `/settle`, no `/supported` endpoint.
|
|
527
606
|
|
|
528
|
-
This flow is real, it is tested, and it
|
|
529
|
-
Sepolia. It
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
`
|
|
534
|
-
|
|
535
|
-
|
|
607
|
+
This flow is real, it is tested, and it opens real escrowed streams on Base
|
|
608
|
+
Sepolia. It does not interoperate with `x402-fetch`, `x402-axios`,
|
|
609
|
+
`x402-express` or any other x402 client or facilitator.
|
|
610
|
+
|
|
611
|
+
Exports: `VerifiedStreamFacilitator`, `METRIK_STREAM_SCHEME`,
|
|
612
|
+
`METRIK_PAYMENT_HEADER`, `encodeStreamPayloadHeader`,
|
|
613
|
+
`parseStreamPayloadHeader`, `parseStreamChallengeJson`, `streamChallengeSchema`,
|
|
614
|
+
`streamOpenPayloadSchema`, `verifiedStreamRequirementsSchema`,
|
|
615
|
+
`StreamPayloadError`, `SpendControlUnavailableError`,
|
|
616
|
+
`SettlementTargetMismatchError`, `DeliveryProofUnavailableError`, and the types
|
|
617
|
+
`StreamFacilitator`, `StreamChallenge`, `StreamOpenPayload`,
|
|
618
|
+
`ConsumedHttpsResponseProofInput`, `VerifiedStreamFacilitatorOptions`,
|
|
619
|
+
`VerifiedStreamRequirements`.
|
|
620
|
+
|
|
621
|
+
Two runtime error names still carry the old prefix: a `StreamPayloadError` has
|
|
622
|
+
`.name === "X402PayloadError"` and a `SpendControlUnavailableError` has
|
|
623
|
+
`.name === "X402SpendControlUnavailableError"`. They were left unchanged so code
|
|
624
|
+
that matches on `error.name` keeps working; prefer `instanceof`.
|
|
536
625
|
|
|
537
626
|
### The header name
|
|
538
627
|
|
|
539
|
-
|
|
540
|
-
x402
|
|
541
|
-
|
|
542
|
-
parse. Metrik's payload now travels as **`X-METRIK-PAYMENT`**. Metrik does not
|
|
543
|
-
put anything on `X-PAYMENT` and will not until it genuinely speaks x402.
|
|
628
|
+
Metrik's payload travels as **`X-METRIK-PAYMENT`**. Metrik puts nothing on the
|
|
629
|
+
x402 v1 `X-PAYMENT` header, and `X-METRIK-PAYMENT` also stays clear of the v2
|
|
630
|
+
header names (`PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE`, `PAYMENT-RESPONSE`).
|
|
544
631
|
|
|
545
|
-
|
|
546
|
-
`PAYMENT-SIGNATURE`, `PAYMENT-RESPONSE`, with no `X-` prefix — so
|
|
547
|
-
`X-METRIK-PAYMENT` stays clear of both the v1 and the v2 namespace.
|
|
632
|
+
---
|
|
548
633
|
|
|
549
|
-
|
|
550
|
-
because there it really is x402. The two never appear on the same request: a
|
|
551
|
-
request carrying both a Metrik capability and an `X-PAYMENT` is refused with
|
|
552
|
-
`400 ambiguous-payment-mode`.
|
|
634
|
+
## Why the metered stream does not fit a shipped x402 scheme
|
|
553
635
|
|
|
554
|
-
|
|
636
|
+
This analysis is why "support x402" was never a wire-format exercise for the
|
|
637
|
+
stream itself. Package facts were gathered on **2026-09-18** from
|
|
638
|
+
`x402-foundation/x402@main` and from `x402@1.2.0` as published on npm; the test
|
|
639
|
+
that re-checked them against the real package was removed with the dependency
|
|
640
|
+
in #115, so re-verify before relying on them.
|
|
555
641
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
equivalent) is in the published package, not just the spec.
|
|
563
|
-
2. **A Metrik / generic-escrow network binding for `auth-capture`,** or a
|
|
564
|
-
redeployment of Metrik settlement behind the canonical `AuthCaptureEscrow`.
|
|
565
|
-
This is a contracts decision, not a package decision — and today's
|
|
566
|
-
`AuthCaptureEscrow` binding has no room for per-second metering or
|
|
567
|
-
oracle-attested checkpoints.
|
|
568
|
-
3. **x402 v2 adoption in the client libraries,** so the extensible `scheme` string
|
|
569
|
-
and CAIP-2 networks in `x402-specification-v2.md` are actually usable.
|
|
570
|
-
|
|
571
|
-
The cheaper intermediate step described in an earlier revision of this document
|
|
572
|
-
— "serve an ordinary `exact` x402 payment for one-shot endpoints alongside the
|
|
573
|
-
Metrik stream flow for metered ones" — **is the step that has now been taken**,
|
|
574
|
-
with one addition that changes its character: the `exact` path is not an
|
|
575
|
-
unverified payment. It is gated on Metrik's own delivery verdict. It is still
|
|
576
|
-
weaker than the stream, for the reason stated at the top: it prevents a payment
|
|
577
|
-
rather than recovering one.
|
|
642
|
+
| Thing | Reality at the date above |
|
|
643
|
+
| ----------------------------------- | ----------------------------------------------------------------- |
|
|
644
|
+
| Spec repository | [`x402-foundation/x402`](https://github.com/x402-foundation/x402) |
|
|
645
|
+
| Schemes implemented by `x402@1.2.0` | **`exact` only** |
|
|
646
|
+
| Schemes that exist _in the spec_ | `exact`, `upto`, `batch-settlement`, `auth-capture` |
|
|
647
|
+
| Protocol versions in `x402@1.2.0` | `x402Version: 1` only; v2 was specified, not yet in that package |
|
|
578
648
|
|
|
579
|
-
|
|
649
|
+
### `exact`
|
|
580
650
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
- the
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
Metrik emits the same `""` rather than inventing a placeholder that looks like
|
|
613
|
-
a real transaction hash.
|
|
614
|
-
|
|
615
|
-
### The gate — [`test/x402-delivery-gate.test.ts`](../test/x402-delivery-gate.test.ts)
|
|
616
|
-
|
|
617
|
-
24 assertions over the real oracle projection and the real decision tree: refuse
|
|
618
|
-
on a fresh failure; allow on a fresh pass; no gate for an untracked resource;
|
|
619
|
-
payee-mismatch refusal; a lying `extra.metrikServiceRef` changing nothing;
|
|
620
|
-
freshness symmetry at the 90 s boundary; every no-evidence case under both
|
|
621
|
-
policies; and `/settle` re-running the gate, refusing to broadcast for a failing
|
|
622
|
-
service, and throwing rather than faking success with no relayer.
|
|
623
|
-
|
|
624
|
-
### The whole loop — [`test/x402-gateway.test.ts`](../test/x402-gateway.test.ts)
|
|
625
|
-
|
|
626
|
-
`x402-fetch@1.2.0` — the published client — reads Metrik's `402`, parses
|
|
627
|
-
`accepts` with the real `PaymentRequirementsSchema`, signs an EIP-3009
|
|
628
|
-
authorization, retries with `X-PAYMENT`, gets `200` plus a
|
|
629
|
-
`X-PAYMENT-RESPONSE` it decodes with the library's own
|
|
630
|
-
`decodeXPaymentResponse`. Nothing in that file hand-rolls a payment header: if
|
|
631
|
-
Metrik's `402` were not conformant, the client would throw instead of paying.
|
|
632
|
-
The same test then flips the oracle to `failing` and asserts the identical, valid
|
|
633
|
-
payment is refused with `402`, nothing is settled, and the seller's upstream is
|
|
634
|
-
never called.
|
|
635
|
-
|
|
636
|
-
**What is real in that test**: the gateway, its HTTP server, the facilitator HTTP
|
|
637
|
-
wire over a real socket, the oracle projection and freshness arithmetic, and the
|
|
638
|
-
EIP-712 signature check. **What is a fixture**: the payer's on-chain balance, and
|
|
639
|
-
the broadcast. Nothing is broadcast, anywhere, ever.
|
|
640
|
-
|
|
641
|
-
Spec and package facts were gathered on **2026-09-18** from
|
|
642
|
-
`x402-foundation/x402@main` and from `x402@1.2.0` as published on npm.
|
|
643
|
-
|
|
644
|
-
One incidental packaging note, true at `x402@1.2.0`: the package's `exports` map
|
|
645
|
-
has no `"."` entry, so `import { … } from "x402"` fails with
|
|
646
|
-
`ERR_PACKAGE_PATH_NOT_EXPORTED` under Node ESM. The subpaths (`x402/types`,
|
|
647
|
-
`x402/facilitator`, `x402/verify`, `x402/schemes`, `x402/client`) are what
|
|
648
|
-
resolve.
|
|
651
|
+
`exact` on EVM is an [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009)
|
|
652
|
+
`transferWithAuthorization`: one fixed amount, one transfer, straight to
|
|
653
|
+
`payTo`. No hold, no conditional release, no partial capture, no reclaim.
|
|
654
|
+
|
|
655
|
+
Metrik's unit of payment is a **metered stream**: the buyer escrows a budget
|
|
656
|
+
into `StreamEscrowV2` at a per-second rate with an expiry, a verifier probes the
|
|
657
|
+
seller each interval, the seller earns only for intervals that verified
|
|
658
|
+
(claimed against Merkle checkpoints), and whatever was never earned is
|
|
659
|
+
recoverable by the buyer via a withdrawal transaction. Collapsing that into one
|
|
660
|
+
up-front transfer to the seller deletes the product — which is exactly what the
|
|
661
|
+
removed v1 integration did, and why it went. The opener path at the top of this
|
|
662
|
+
document keeps the `exact` payment but points its `payTo` at an opener that
|
|
663
|
+
opens the stream, instead of at the seller.
|
|
664
|
+
|
|
665
|
+
### `upto`
|
|
666
|
+
|
|
667
|
+
`upto` (authorize a maximum, settle actual usage) fits a single metered
|
|
668
|
+
_request_, but its spec explicitly puts "multi-settlement / streaming" out of
|
|
669
|
+
scope and requires each authorization to be settled at most once. A Metrik
|
|
670
|
+
stream settles the same authorization repeatedly, once per verified checkpoint.
|
|
671
|
+
|
|
672
|
+
### `auth-capture`
|
|
673
|
+
|
|
674
|
+
`auth-capture` in its `escrow` flow is the closest conceptual match —
|
|
675
|
+
`authorize`/`capture`/`void`/`reclaim` map onto `openStream`/`claim`/`close`/
|
|
676
|
+
`reclaim` — but its EVM binding is pinned to the canonical `AuthCaptureEscrow`
|
|
677
|
+
contract (Base's Commerce Payments Protocol), which a conformant facilitator
|
|
678
|
+
must read and whose events it must observe. Metrik settles in `StreamEscrowV2`
|
|
679
|
+
(`0x0f09f36Ccc05A7c9882F438721C08De314dFd46C`), a different contract with a
|
|
680
|
+
different state model, so conformance there would be a contract deployment, not
|
|
681
|
+
a code change in this package.
|