@absol-labs/agent 0.9.1 → 0.11.0
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 +184 -26
- package/dist/capability/invocation-capability.d.ts +18 -6
- package/dist/capability/invocation-capability.d.ts.map +1 -1
- package/dist/capability/invocation-capability.js.map +1 -1
- package/dist/cli/amounts.d.ts +26 -0
- package/dist/cli/amounts.d.ts.map +1 -0
- package/dist/cli/amounts.js +71 -0
- package/dist/cli/amounts.js.map +1 -0
- package/dist/cli/args.d.ts +32 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +119 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/attesters.d.ts +51 -0
- package/dist/cli/attesters.d.ts.map +1 -0
- package/dist/cli/attesters.js +58 -0
- package/dist/cli/attesters.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +5 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/claim.d.ts +13 -0
- package/dist/cli/commands/claim.d.ts.map +1 -0
- package/dist/cli/commands/claim.js +102 -0
- package/dist/cli/commands/claim.js.map +1 -0
- package/dist/cli/commands/close.d.ts +3 -0
- package/dist/cli/commands/close.d.ts.map +1 -0
- package/dist/cli/commands/close.js +55 -0
- package/dist/cli/commands/close.js.map +1 -0
- package/dist/cli/commands/discover.d.ts +3 -0
- package/dist/cli/commands/discover.d.ts.map +1 -0
- package/dist/cli/commands/discover.js +183 -0
- package/dist/cli/commands/discover.js.map +1 -0
- package/dist/cli/commands/invoke.d.ts +3 -0
- package/dist/cli/commands/invoke.d.ts.map +1 -0
- package/dist/cli/commands/invoke.js +142 -0
- package/dist/cli/commands/invoke.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +113 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/open.d.ts +3 -0
- package/dist/cli/commands/open.d.ts.map +1 -0
- package/dist/cli/commands/open.js +331 -0
- package/dist/cli/commands/open.js.map +1 -0
- package/dist/cli/commands/reclaim-unverified.d.ts +3 -0
- package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -0
- package/dist/cli/commands/reclaim-unverified.js +172 -0
- package/dist/cli/commands/reclaim-unverified.js.map +1 -0
- package/dist/cli/commands/reclaim.d.ts +33 -0
- package/dist/cli/commands/reclaim.d.ts.map +1 -0
- package/dist/cli/commands/reclaim.js +308 -0
- package/dist/cli/commands/reclaim.js.map +1 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +43 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/streams.d.ts +3 -0
- package/dist/cli/commands/streams.d.ts.map +1 -0
- package/dist/cli/commands/streams.js +257 -0
- package/dist/cli/commands/streams.js.map +1 -0
- package/dist/cli/commands/watch.d.ts +3 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/commands/watch.js +55 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/config.d.ts +61 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +140 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/context.d.ts +29 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +2 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/deps.d.ts +191 -0
- package/dist/cli/deps.d.ts.map +1 -0
- package/dist/cli/deps.js +277 -0
- package/dist/cli/deps.js.map +1 -0
- package/dist/cli/descriptor.d.ts +61 -0
- package/dist/cli/descriptor.d.ts.map +1 -0
- package/dist/cli/descriptor.js +78 -0
- package/dist/cli/descriptor.js.map +1 -0
- package/dist/cli/errors.d.ts +48 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +129 -0
- package/dist/cli/errors.js.map +1 -0
- package/dist/cli/escape-hatch.d.ts +50 -0
- package/dist/cli/escape-hatch.d.ts.map +1 -0
- package/dist/cli/escape-hatch.js +45 -0
- package/dist/cli/escape-hatch.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/listings.d.ts +15 -0
- package/dist/cli/listings.d.ts.map +1 -0
- package/dist/cli/listings.js +56 -0
- package/dist/cli/listings.js.map +1 -0
- package/dist/cli/mandate.d.ts +66 -0
- package/dist/cli/mandate.d.ts.map +1 -0
- package/dist/cli/mandate.js +61 -0
- package/dist/cli/mandate.js.map +1 -0
- package/dist/cli/output.d.ts +33 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +34 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/preflight.d.ts +38 -0
- package/dist/cli/preflight.d.ts.map +1 -0
- package/dist/cli/preflight.js +72 -0
- package/dist/cli/preflight.js.map +1 -0
- package/dist/cli/privy.d.ts +83 -0
- package/dist/cli/privy.d.ts.map +1 -0
- package/dist/cli/privy.js +230 -0
- package/dist/cli/privy.js.map +1 -0
- package/dist/cli/progress.d.ts +36 -0
- package/dist/cli/progress.d.ts.map +1 -0
- package/dist/cli/progress.js +40 -0
- package/dist/cli/progress.js.map +1 -0
- package/dist/cli/run.d.ts +25 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +173 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/scan-range.d.ts +55 -0
- package/dist/cli/scan-range.d.ts.map +1 -0
- package/dist/cli/scan-range.js +57 -0
- package/dist/cli/scan-range.js.map +1 -0
- package/dist/cli/spend-policy.d.ts +60 -0
- package/dist/cli/spend-policy.d.ts.map +1 -0
- package/dist/cli/spend-policy.js +65 -0
- package/dist/cli/spend-policy.js.map +1 -0
- package/dist/cli/status.d.ts +90 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +252 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/timeout.d.ts +30 -0
- package/dist/cli/timeout.d.ts.map +1 -0
- package/dist/cli/timeout.js +55 -0
- package/dist/cli/timeout.js.map +1 -0
- package/dist/cli/version.d.ts +12 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +22 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli/wallet.d.ts +164 -0
- package/dist/cli/wallet.d.ts.map +1 -0
- package/dist/cli/wallet.js +234 -0
- package/dist/cli/wallet.js.map +1 -0
- package/dist/frameworks/crewai.d.ts +10 -0
- package/dist/frameworks/crewai.d.ts.map +1 -1
- package/dist/frameworks/crewai.js +12 -2
- package/dist/frameworks/crewai.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +38 -3
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +25 -1
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +53 -0
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +189 -4
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/server-entry.d.ts +1 -0
- package/dist/gateway/server-entry.js +1 -0
- package/dist/gateway/server-entry.js.map +1 -1
- package/dist/gateway/x402-mode.d.ts +123 -0
- package/dist/gateway/x402-mode.d.ts.map +1 -0
- package/dist/gateway/x402-mode.js +212 -0
- package/dist/gateway/x402-mode.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/mandates/env.d.ts +3 -2
- package/dist/mandates/env.d.ts.map +1 -1
- package/dist/mandates/env.js +3 -2
- package/dist/mandates/env.js.map +1 -1
- package/dist/mcp/http-server.d.ts +1 -0
- package/dist/mcp/http-server.js +1 -0
- package/dist/mcp/http-server.js.map +1 -1
- package/dist/mcp/server.d.ts +49 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +533 -25
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/service-invoker.d.ts +97 -0
- package/dist/mcp/service-invoker.d.ts.map +1 -0
- package/dist/mcp/service-invoker.js +167 -0
- package/dist/mcp/service-invoker.js.map +1 -0
- package/dist/mcp/stdio.d.ts +1 -0
- package/dist/mcp/stdio.js +1 -0
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/sdk/client.d.ts +25 -1
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +20 -1
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/invoke.d.ts +19 -4
- package/dist/sdk/invoke.d.ts.map +1 -1
- package/dist/sdk/invoke.js +69 -5
- package/dist/sdk/invoke.js.map +1 -1
- package/dist/x402/delivery-oracle.d.ts +108 -0
- package/dist/x402/delivery-oracle.d.ts.map +1 -0
- package/dist/x402/delivery-oracle.js +174 -0
- package/dist/x402/delivery-oracle.js.map +1 -0
- package/dist/x402/facilitator-server-entry.d.ts +3 -0
- package/dist/x402/facilitator-server-entry.d.ts.map +1 -0
- package/dist/x402/facilitator-server-entry.js +27 -0
- package/dist/x402/facilitator-server-entry.js.map +1 -0
- package/dist/x402/facilitator-server.d.ts +73 -0
- package/dist/x402/facilitator-server.d.ts.map +1 -0
- package/dist/x402/facilitator-server.js +305 -0
- package/dist/x402/facilitator-server.js.map +1 -0
- package/dist/x402/facilitator.d.ts +69 -17
- package/dist/x402/facilitator.d.ts.map +1 -1
- package/dist/x402/facilitator.js +76 -33
- package/dist/x402/facilitator.js.map +1 -1
- package/dist/x402/payment-requirements.d.ts +106 -0
- package/dist/x402/payment-requirements.d.ts.map +1 -0
- package/dist/x402/payment-requirements.js +138 -0
- package/dist/x402/payment-requirements.js.map +1 -0
- package/dist/x402/relayer.d.ts +41 -0
- package/dist/x402/relayer.d.ts.map +1 -0
- package/dist/x402/relayer.js +64 -0
- package/dist/x402/relayer.js.map +1 -0
- package/dist/x402/resource-index.d.ts +75 -0
- package/dist/x402/resource-index.d.ts.map +1 -0
- package/dist/x402/resource-index.js +126 -0
- package/dist/x402/resource-index.js.map +1 -0
- package/dist/x402/verified-delivery-facilitator.d.ts +285 -0
- package/dist/x402/verified-delivery-facilitator.d.ts.map +1 -0
- package/dist/x402/verified-delivery-facilitator.js +423 -0
- package/dist/x402/verified-delivery-facilitator.js.map +1 -0
- package/dist/zktls/t2-delivery-proof.d.ts +6 -6
- package/docs/agent-layer.md +14 -9
- package/docs/cli.md +705 -0
- package/docs/crewai.md +6 -2
- package/docs/eliza.md +6 -1
- package/docs/mcp-hosted.md +7 -5
- package/docs/mcp-reference.md +175 -0
- package/docs/quickstart.md +36 -9
- package/docs/threat-model.md +4 -3
- package/docs/x402.md +648 -0
- package/package.json +33 -10
- package/src/capability/invocation-capability.ts +17 -1
- package/src/cli/amounts.ts +102 -0
- package/src/cli/args.ts +150 -0
- package/src/cli/attesters.ts +86 -0
- package/src/cli/bin.ts +5 -0
- package/src/cli/commands/claim.ts +126 -0
- package/src/cli/commands/close.ts +69 -0
- package/src/cli/commands/discover.ts +228 -0
- package/src/cli/commands/invoke.ts +178 -0
- package/src/cli/commands/mcp.ts +135 -0
- package/src/cli/commands/open.ts +397 -0
- package/src/cli/commands/reclaim-unverified.ts +218 -0
- package/src/cli/commands/reclaim.ts +377 -0
- package/src/cli/commands/status.ts +56 -0
- package/src/cli/commands/streams.ts +344 -0
- package/src/cli/commands/watch.ts +70 -0
- package/src/cli/config.ts +208 -0
- package/src/cli/context.ts +30 -0
- package/src/cli/deps.ts +528 -0
- package/src/cli/descriptor.ts +133 -0
- package/src/cli/errors.ts +162 -0
- package/src/cli/escape-hatch.ts +92 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/listings.ts +77 -0
- package/src/cli/mandate.ts +126 -0
- package/src/cli/output.ts +63 -0
- package/src/cli/preflight.ts +88 -0
- package/src/cli/privy.ts +396 -0
- package/src/cli/progress.ts +75 -0
- package/src/cli/run.ts +209 -0
- package/src/cli/scan-range.ts +76 -0
- package/src/cli/spend-policy.ts +139 -0
- package/src/cli/status.ts +381 -0
- package/src/cli/timeout.ts +64 -0
- package/src/cli/version.ts +21 -0
- package/src/cli/wallet.ts +372 -0
- package/src/frameworks/crewai.ts +13 -2
- package/src/gateway/caller-auth-gateway.ts +77 -6
- package/src/gateway/http-server.ts +280 -6
- package/src/gateway/server-entry.ts +1 -0
- package/src/gateway/x402-mode.ts +350 -0
- package/src/index.ts +40 -5
- package/src/mandates/env.ts +3 -2
- package/src/mcp/http-server.ts +1 -0
- package/src/mcp/server.ts +706 -30
- package/src/mcp/service-invoker.ts +285 -0
- package/src/mcp/stdio.ts +1 -0
- package/src/sdk/client.ts +33 -1
- package/src/sdk/invoke.ts +90 -12
- package/src/x402/delivery-oracle.ts +285 -0
- package/src/x402/facilitator-server-entry.ts +35 -0
- package/src/x402/facilitator-server.ts +445 -0
- package/src/x402/facilitator.ts +125 -46
- package/src/x402/payment-requirements.ts +231 -0
- package/src/x402/relayer.ts +122 -0
- package/src/x402/resource-index.ts +228 -0
- package/src/x402/verified-delivery-facilitator.ts +744 -0
package/docs/x402.md
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
# Metrik and x402
|
|
2
|
+
|
|
3
|
+
> **Metrik ships a real, spec-compliant x402 facilitator — one that refuses to
|
|
4
|
+
> settle payment for a service its own oracle says is not delivering.** It does
|
|
5
|
+
> NOT express its metered escrow stream as an x402 payment, and it never will
|
|
6
|
+
> under the schemes x402 ships today.
|
|
7
|
+
>
|
|
8
|
+
> Those two sentences are the whole story, and the rest of this document is the
|
|
9
|
+
> evidence for both halves. Earlier versions of this file said flatly that "this
|
|
10
|
+
> package does not ship an x402 facilitator". That was true when it was written
|
|
11
|
+
> and is now out of date; the analysis it was built on is unchanged and still
|
|
12
|
+
> below.
|
|
13
|
+
|
|
14
|
+
The tagline — _x402 proves the payment, Metrik proves the delivery_ — used to be
|
|
15
|
+
positioning only. It is now literally what the code does: a standard `exact`
|
|
16
|
+
payment, verified by a standard facilitator, with one extra question asked
|
|
17
|
+
before settlement that no other facilitator can ask.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## The two things Metrik does with 402, and how they differ
|
|
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 }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`isValid` is a boolean and the spec does not constrain what a facilitator may
|
|
54
|
+
consider before returning `false`. A facilitator is the party a resource server
|
|
55
|
+
delegates its "should this payment go through" decision to. Metrik simply knows
|
|
56
|
+
one more thing than every other facilitator, because it runs a prober.
|
|
57
|
+
|
|
58
|
+
So: standard scheme, standard headers, standard client, no contract change — and
|
|
59
|
+
a property no other facilitator can offer, because no other facilitator has an
|
|
60
|
+
oracle.
|
|
61
|
+
|
|
62
|
+
### What `isValid: true` from Metrik does and does not mean
|
|
63
|
+
|
|
64
|
+
It means **"this payment is well-formed and Metrik has no fresh evidence that
|
|
65
|
+
this service is failing."** It is not an endorsement, not a correctness
|
|
66
|
+
guarantee, and not a promise about the next call. The gate only ever subtracts.
|
|
67
|
+
|
|
68
|
+
And per the standing rule: Metrik verifies **delivery**, not **correctness**. A
|
|
69
|
+
service that returns a fast, well-formed, wrong answer passes every check Metrik
|
|
70
|
+
makes.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Tier 1: the gateway speaks 402
|
|
75
|
+
|
|
76
|
+
`CallerAuthGateway` authorizes requests carrying a Metrik `InvocationCapability`
|
|
77
|
+
— a buyer with an open, funded, verified stream. That path is **unchanged**. A
|
|
78
|
+
request with a valid capability behaves today exactly as it did before.
|
|
79
|
+
|
|
80
|
+
What is new is opt-in: set `METRIK_GATEWAY_X402_PRICE_BASE_UNITS` and a request
|
|
81
|
+
with **no** capability gets a real x402 `402` instead of a `403`:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"x402Version": 1,
|
|
86
|
+
"error": "X-PAYMENT header is required",
|
|
87
|
+
"accepts": [
|
|
88
|
+
{
|
|
89
|
+
"scheme": "exact",
|
|
90
|
+
"network": "base-sepolia",
|
|
91
|
+
"maxAmountRequired": "1000",
|
|
92
|
+
"resource": "https://livedata.example/price",
|
|
93
|
+
"payTo": "0x...",
|
|
94
|
+
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
95
|
+
"maxTimeoutSeconds": 60,
|
|
96
|
+
"extra": { "name": "USDC", "version": "2", "metrikServiceRef": "0x..." }
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Three rules govern it:
|
|
103
|
+
|
|
104
|
+
1. **The two paths never overlap.** Capability present -> stream path. Capability
|
|
105
|
+
absent -> `402`. **Both present -> `400 ambiguous-payment-mode`**, refused
|
|
106
|
+
rather than guessed: the failure mode of guessing is charging the caller
|
|
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
|
+
---
|
|
145
|
+
|
|
146
|
+
## Tier 2: the verified-delivery facilitator
|
|
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:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"isValid": false,
|
|
253
|
+
"invalidReason": "invalid_payment_requirements",
|
|
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
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`VerifyResponseSchema` is a `strip` (not `strict`) Zod object, so this is
|
|
268
|
+
spec-tolerated: a conformant client parses the response successfully and simply
|
|
269
|
+
drops `metrik`. A client that wants the reason reads it off the raw body. There
|
|
270
|
+
is a test asserting exactly that.
|
|
271
|
+
|
|
272
|
+
### Staleness: a 90-second window, and why
|
|
273
|
+
|
|
274
|
+
A verdict from ten minutes ago is not evidence that a service is alive now, so
|
|
275
|
+
freshness is the crux. The oracle records a status per active stream on its
|
|
276
|
+
checkpoint cadence of **~15–22 s**. The window has to be wide enough that
|
|
277
|
+
ordinary jitter does not flap the gate and narrow enough that "verified" still
|
|
278
|
+
means "now":
|
|
279
|
+
|
|
280
|
+
- three worst-case probe intervals (3 x 22 s = **66 s**) absorbs two consecutive
|
|
281
|
+
missed or slow ticks, which is operational noise rather than a dead service;
|
|
282
|
+
- plus the oracle's record-and-serve latency and the facilitator's fetch round
|
|
283
|
+
trip, rounded up: **90 s**.
|
|
284
|
+
|
|
285
|
+
Tighter (say 30 s) and one slow tick turns a healthy service indeterminate.
|
|
286
|
+
Looser and a service that died four minutes ago still reads `delivering`, which
|
|
287
|
+
is precisely the claim this gate exists to avoid making. Note the oracle's own
|
|
288
|
+
TTL is **300 s**, so this window is strictly stricter: a verdict aged 90–300 s
|
|
289
|
+
reads `delivering` at the source and `indeterminate` here.
|
|
290
|
+
|
|
291
|
+
Staleness is applied **symmetrically**. A stale _failure_ is not evidence either,
|
|
292
|
+
so one bad probe cannot silently refuse forever.
|
|
293
|
+
|
|
294
|
+
### No fresh evidence: allow, by default
|
|
295
|
+
|
|
296
|
+
There are three distinct ways to have no opinion, and they are never collapsed
|
|
297
|
+
into "failing":
|
|
298
|
+
|
|
299
|
+
| Situation | `metrik.gate` |
|
|
300
|
+
| ------------------------------------------------------------------------- | -------------------------------------- |
|
|
301
|
+
| Service has no row at all (no active stream), or oracle says `unverified` | `indeterminate` |
|
|
302
|
+
| Verdict older than the freshness window | `indeterminate` (`verdict-stale`) |
|
|
303
|
+
| Oracle unreachable or malformed | `indeterminate` (`oracle-unreachable`) |
|
|
304
|
+
| Metrik cannot read its own registry | `registry-unavailable` |
|
|
305
|
+
|
|
306
|
+
All of them **allow** by default. The standing Metrik rule is buyer-favouring
|
|
307
|
+
and still is — but that rule governs the case where Metrik **holds** the buyer's
|
|
308
|
+
money and must decide whether to release it. Here Metrik holds nothing, so
|
|
309
|
+
refusing does not protect anyone's funds; it only denies a service the buyer
|
|
310
|
+
asked to buy, on a rail that works fine without Metrik. Four reasons:
|
|
311
|
+
|
|
312
|
+
1. **Absent evidence is the normal case.** The oracle's `/services` projection is
|
|
313
|
+
built from ACTIVE STREAMS. A service with no live Metrik stream has no row at
|
|
314
|
+
all — on the live oracle today, `GET /services` returns `{"services":[]}`.
|
|
315
|
+
Failing closed would refuse **100% of payments**. That is a broken
|
|
316
|
+
facilitator, not a careful one.
|
|
317
|
+
2. **Fail-closed is a censorship lever.** If "oracle down" meant "no payments",
|
|
318
|
+
anyone who could degrade Metrik's oracle could block payments to a competitor
|
|
319
|
+
listed on it. `allow` removes that lever.
|
|
320
|
+
3. **The harms are asymmetric.** A wrong refusal denies the service outright. A
|
|
321
|
+
wrong allowance passes one `exact` payment, at a price the buyer already
|
|
322
|
+
agreed to, worth one call.
|
|
323
|
+
4. **A pass never claims anything.** `isValid: true` with `gate: "indeterminate"`
|
|
324
|
+
says "no fresh evidence", in the response, in words.
|
|
325
|
+
|
|
326
|
+
A **fresh** failure is refused under every policy.
|
|
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
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
# The facilitator
|
|
338
|
+
METRIK_X402_RPC_URL=https://sepolia.base.org \
|
|
339
|
+
METRIK_X402_RELAYER_PRIVATE_KEY=0x... \
|
|
340
|
+
npx metrik-x402-facilitator
|
|
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
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
The facilitator **refuses to start** without `METRIK_X402_RELAYER_PRIVATE_KEY`
|
|
350
|
+
unless `METRIK_X402_ALLOW_VERIFY_ONLY=true` is set explicitly. A facilitator that
|
|
351
|
+
accepts `/settle` and quietly never broadcasts is worse than one that is absent.
|
|
352
|
+
|
|
353
|
+
### Settlement is built but has never broadcast anything
|
|
354
|
+
|
|
355
|
+
`POST /settle` is implemented end to end and delegates to the real `x402`
|
|
356
|
+
`settle()`, which submits the EIP-3009 `transferWithAuthorization`. **It has
|
|
357
|
+
never been run against a live chain from this repo**, because that needs a
|
|
358
|
+
relayer wallet funded with gas, which does not exist yet. What is missing is a
|
|
359
|
+
key and gas, not code. Until then:
|
|
360
|
+
|
|
361
|
+
- no relayer key -> the server will not start (or runs verify-only and returns
|
|
362
|
+
`503` on `/settle`);
|
|
363
|
+
- every test settles through a recording fixture that broadcasts nothing.
|
|
364
|
+
|
|
365
|
+
Nothing in this repo has ever sent an x402 settlement transaction.
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## What x402 actually is, as shipped
|
|
370
|
+
|
|
371
|
+
All of the following was verified against the published packages and the
|
|
372
|
+
specification repository (see [Evidence](#evidence) for how, and when).
|
|
373
|
+
|
|
374
|
+
| Thing | Reality |
|
|
375
|
+
| ---------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
376
|
+
| Spec repository | [`x402-foundation/x402`](https://github.com/x402-foundation/x402) |
|
|
377
|
+
| Published client/server packages | `x402`, `x402-fetch`, `x402-axios`, `x402-express`, `x402-hono`, `x402-next` |
|
|
378
|
+
| Version of `x402` on npm | **1.2.0**, Apache-2.0, no `engines` constraint |
|
|
379
|
+
| Protocol versions the package implements | **`x402Version: 1` only** (`x402Versions === [1]`) |
|
|
380
|
+
| Payment schemes the package implements | **`exact` only** (`schemes === ["exact"]`) |
|
|
381
|
+
| Schemes that exist _in the spec_ | `exact`, `upto`, `batch-settlement`, `auth-capture` |
|
|
382
|
+
| Transport (v1, shipped) | HTTP `402` + `X-PAYMENT` request header + `X-PAYMENT-RESPONSE` response header |
|
|
383
|
+
| Transport (v2, spec-only) | `PAYMENT-REQUIRED` / `PAYMENT-SIGNATURE` / `PAYMENT-RESPONSE` headers — **renamed**, no `X-` prefix |
|
|
384
|
+
| Facilitator interface | `POST /verify`, `POST /settle`, `GET /supported` |
|
|
385
|
+
|
|
386
|
+
Two gaps matter enormously and are easy to miss:
|
|
387
|
+
|
|
388
|
+
1. **The spec is ahead of the shipped code.** `auth-capture` and `upto` are
|
|
389
|
+
written up under `specs/schemes/` but are _not_ in `x402@1.2.0`. The package's
|
|
390
|
+
`PaymentRequirementsSchema` parses `scheme` as a closed Zod enum containing
|
|
391
|
+
exactly one value, `"exact"`. A `PaymentRequirements` naming any other scheme —
|
|
392
|
+
including `auth-capture` and `upto` — is **rejected by the real library**.
|
|
393
|
+
2. **x402 v2 is specified but not shipped.** `x402-specification-v2.md` defines
|
|
394
|
+
`x402Version: 2` with CAIP-2 network identifiers (`eip155:8453`) and an
|
|
395
|
+
explicitly extensible `scheme` _string_. `x402@1.2.0` implements v1, whose
|
|
396
|
+
`network` is a closed enum of bare names (`base-sepolia`, `base`, …). So the
|
|
397
|
+
extensibility that would make room for a Metrik scheme exists on paper, not in
|
|
398
|
+
the library your users will install.
|
|
399
|
+
|
|
400
|
+
### The `exact` scheme, concretely
|
|
401
|
+
|
|
402
|
+
`exact` on EVM is an [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009)
|
|
403
|
+
`transferWithAuthorization`: the client signs `{from, to, value, validAfter,
|
|
404
|
+
validBefore, nonce}`, and the facilitator submits exactly that one transfer.
|
|
405
|
+
|
|
406
|
+
That is the whole scheme. One fixed amount. One transfer. Straight to the payee.
|
|
407
|
+
**No escrow, no hold, no conditional release, no partial capture, no refund, no
|
|
408
|
+
reclaim.**
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Why Metrik's STREAM does not fit `exact`
|
|
413
|
+
|
|
414
|
+
Metrik's unit of payment is a **metered stream**, not a transfer:
|
|
415
|
+
|
|
416
|
+
- the buyer escrows a budget into `StreamEscrowV2` at a per-second rate with an expiry;
|
|
417
|
+
- a verifier probes the seller's endpoint each interval;
|
|
418
|
+
- the seller earns **only for intervals that verified**, claimed incrementally
|
|
419
|
+
against Merkle checkpoints;
|
|
420
|
+
- whatever was never earned is **recoverable by the buyer** via a withdrawal
|
|
421
|
+
transaction.
|
|
422
|
+
|
|
423
|
+
Mapping THAT onto `exact` would require collapsing it to a single fixed payment
|
|
424
|
+
made up front, directly to the seller, with no escrow and no recoverability —
|
|
425
|
+
which deletes the product. So the stream is not, and will not be, expressed as an
|
|
426
|
+
x402 payment.
|
|
427
|
+
|
|
428
|
+
This is exactly why the delivery gate exists in the shape it does. The danger was
|
|
429
|
+
never "support x402"; it was a developer seeing `x402-fetch` succeed against a
|
|
430
|
+
Metrik endpoint and concluding their money was protected by verified delivery.
|
|
431
|
+
The gate is honest about being weaker: it prevents a bad payment, it does not
|
|
432
|
+
recover one, and it says so in the response (`metrik.detail`), in this document,
|
|
433
|
+
and in the table at the top.
|
|
434
|
+
|
|
435
|
+
## Why `upto` does not fit either
|
|
436
|
+
|
|
437
|
+
`upto` (authorize a maximum, settle the actual usage) is closer, and for a
|
|
438
|
+
single metered _request_ it would be a good fit. But its spec rules it out
|
|
439
|
+
explicitly:
|
|
440
|
+
|
|
441
|
+
> **Out of Scope** … **Multi-settlement / streaming**: Settling the same
|
|
442
|
+
> authorization multiple times (e.g., pay-per-chunk streaming)
|
|
443
|
+
|
|
444
|
+
and
|
|
445
|
+
|
|
446
|
+
> ### 1. Single-Use Authorization
|
|
447
|
+
>
|
|
448
|
+
> Each authorization MUST be settled at most once.
|
|
449
|
+
|
|
450
|
+
A Metrik stream settles the _same_ authorization repeatedly, once per verified
|
|
451
|
+
checkpoint, for its whole lifetime. That is precisely the case `upto` excludes.
|
|
452
|
+
|
|
453
|
+
## `auth-capture` is the right shape for the STREAM — and still out of reach
|
|
454
|
+
|
|
455
|
+
Of the four specified schemes, [`auth-capture`](https://github.com/x402-foundation/x402/blob/main/specs/schemes/auth-capture/scheme_auth_capture.md)
|
|
456
|
+
in its `escrow` payment flow is a near-exact conceptual match for Metrik:
|
|
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.
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## What this package actually ships
|
|
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
|
|
498
|
+
|
|
499
|
+
| Piece | File |
|
|
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):
|
|
517
|
+
|
|
518
|
+
- scheme string **`streamproof-verified-stream`** — deliberately _not_ an x402
|
|
519
|
+
scheme name, and not registered with x402;
|
|
520
|
+
- a challenge body of `{scheme, requirements}` where `requirements` is
|
|
521
|
+
Metrik-specific (`operator`, `serviceRef`, `ratePerSecondUsdc`,
|
|
522
|
+
`maxDurationSeconds`, `maxBudgetUsdc`, `escrow`, `chainId`) — **not** x402
|
|
523
|
+
`PaymentRequirements`;
|
|
524
|
+
- a payload header of `` `${scheme} ${base64url(JSON)}` `` carrying the challenge
|
|
525
|
+
plus an owner-signed `SpendMandate` — **not** an x402 `PaymentPayload`;
|
|
526
|
+
- no `/verify`, no `/settle`, no `/supported` endpoint.
|
|
527
|
+
|
|
528
|
+
This flow is real, it is tested, and it does open real escrowed streams on Base
|
|
529
|
+
Sepolia. It is simply **not x402**. It is named for the HTTP status code it
|
|
530
|
+
reuses, and for the x402 design it was modelled on.
|
|
531
|
+
|
|
532
|
+
The class is exported as `VerifiedStreamFacilitator`. The older
|
|
533
|
+
`VerifiedStreamX402Facilitator` name and the other `X402*` exports still work and
|
|
534
|
+
are kept as deprecated aliases so nothing breaks, but they are misnomers and the
|
|
535
|
+
non-`X402` names are canonical.
|
|
536
|
+
|
|
537
|
+
### The header name
|
|
538
|
+
|
|
539
|
+
Until this change, the integration test sent the Metrik payload under the real
|
|
540
|
+
x402 header name `X-PAYMENT`. That was the most actively misleading part of the
|
|
541
|
+
whole surface — a real x402 header carrying a payload no x402 facilitator can
|
|
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.
|
|
544
|
+
|
|
545
|
+
Note that x402 v2 renames its headers entirely — `PAYMENT-REQUIRED`,
|
|
546
|
+
`PAYMENT-SIGNATURE`, `PAYMENT-RESPONSE`, with no `X-` prefix — so
|
|
547
|
+
`X-METRIK-PAYMENT` stays clear of both the v1 and the v2 namespace.
|
|
548
|
+
|
|
549
|
+
The gateway's x402 mode uses the real `X-PAYMENT` / `X-PAYMENT-RESPONSE` headers,
|
|
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`.
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## What would have to be true for the STREAM to be an x402 payment
|
|
557
|
+
|
|
558
|
+
In rough order of cost:
|
|
559
|
+
|
|
560
|
+
1. **`x402@>=?` ships a scheme with repeatable settlement.** Today `schemes ===
|
|
561
|
+
["exact"]`. Nothing Metrik-shaped can be expressed until `auth-capture` (or an
|
|
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.
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## Evidence
|
|
582
|
+
|
|
583
|
+
Everything here is checked against the **real** published `x402` package and the
|
|
584
|
+
**real** published `x402-fetch` client — devDependencies, not fixtures.
|
|
585
|
+
|
|
586
|
+
### The stream is not x402 — [`test/x402-conformance.test.ts`](../test/x402-conformance.test.ts)
|
|
587
|
+
|
|
588
|
+
- `schemes` is exactly `["exact"]`;
|
|
589
|
+
- `x402Versions` is exactly `[1]`;
|
|
590
|
+
- the real `PaymentRequirementsSchema` **rejects** Metrik's stream scheme string;
|
|
591
|
+
- it also rejects `auth-capture` and `upto`;
|
|
592
|
+
- the real `PaymentPayloadSchema` rejects Metrik's stream payload shape;
|
|
593
|
+
- the stream flow publishes nothing on the `X-PAYMENT` header.
|
|
594
|
+
|
|
595
|
+
That test is a tripwire. **When x402 ships a scheme that fits the Metrik stream,
|
|
596
|
+
those assertions start failing** — the signal to revisit this document.
|
|
597
|
+
|
|
598
|
+
### What Metrik emits IS x402 — [`test/x402-facilitator-conformance.test.ts`](../test/x402-facilitator-conformance.test.ts)
|
|
599
|
+
|
|
600
|
+
Everything Metrik puts on the wire is parsed with the real schemas: the
|
|
601
|
+
`PaymentRequirements`, the `402` body the way `x402-fetch` parses it, and both
|
|
602
|
+
the passing and the refusing `VerifyResponse`. It also pins two places where the
|
|
603
|
+
shipped library contradicts itself, so they are recorded decisions and not
|
|
604
|
+
accidents:
|
|
605
|
+
|
|
606
|
+
- `x402ResponseSchema` types `error` as the closed `ErrorReasons` enum, which no
|
|
607
|
+
real x402 server obeys (`x402-express` puts human text there, `x402-fetch`
|
|
608
|
+
never reads the field). Metrik follows the shipped behaviour.
|
|
609
|
+
- `SettleResponseSchema` requires `transaction` to match an address-shaped regex,
|
|
610
|
+
so a FAILED settlement cannot be expressed conformantly. `x402`'s own `settle`
|
|
611
|
+
returns `transaction: ""` on every error path, which its own schema rejects.
|
|
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.
|