@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/src/x402/facilitator.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrik-native HTTP `402` challenge flow.
|
|
3
|
+
*
|
|
4
|
+
* This is NOT an implementation of the x402 protocol. It uses its own scheme
|
|
5
|
+
* string (`streamproof-verified-stream`), its own wire format, and its own
|
|
6
|
+
* request header (`x-metrik-payment`). It does not interoperate with
|
|
7
|
+
* `x402-fetch`, `x402-axios`, `x402-express`, or any other x402 client or
|
|
8
|
+
* facilitator. See `docs/x402.md` for the full explanation of why Metrik's
|
|
9
|
+
* metered-stream settlement does not fit any shipped x402 scheme.
|
|
10
|
+
*
|
|
11
|
+
* The `X402*` / `x402*` exports at the bottom of this file are deprecated
|
|
12
|
+
* aliases of the canonical names, kept so existing consumers keep working.
|
|
13
|
+
*/
|
|
1
14
|
import type { StreamProofClientConfig } from "@absol-labs/sdk";
|
|
2
15
|
import { z } from "zod";
|
|
3
16
|
|
|
@@ -16,7 +29,19 @@ import {
|
|
|
16
29
|
type VerifiedConsumedHttpsResponseProof,
|
|
17
30
|
} from "../zktls/reclaim.js";
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Immutable wire constant for the Metrik-native 402 scheme. The value is
|
|
34
|
+
* historical (`streamproof-*` predates the Metrik rename) and MUST NOT change:
|
|
35
|
+
* the live gateway and deployed callers depend on it.
|
|
36
|
+
*/
|
|
37
|
+
export const METRIK_STREAM_SCHEME = "streamproof-verified-stream" as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Request header carrying the Metrik verified-stream payload. Deliberately NOT
|
|
41
|
+
* `x-payment`: that header belongs to the x402 protocol, which this flow does
|
|
42
|
+
* not implement.
|
|
43
|
+
*/
|
|
44
|
+
export const METRIK_PAYMENT_HEADER = "x-metrik-payment" as const;
|
|
20
45
|
|
|
21
46
|
const addressSchema = z.string().regex(/^0x[0-9a-fA-F]{40}$/);
|
|
22
47
|
const bytes32Schema = z.string().regex(/^0x[0-9a-fA-F]{64}$/);
|
|
@@ -35,36 +60,36 @@ export interface VerifiedStreamRequirements extends z.infer<
|
|
|
35
60
|
typeof verifiedStreamRequirementsSchema
|
|
36
61
|
> {}
|
|
37
62
|
|
|
38
|
-
export const
|
|
39
|
-
scheme: z.literal(
|
|
63
|
+
export const streamChallengeSchema = z.object({
|
|
64
|
+
scheme: z.literal(METRIK_STREAM_SCHEME),
|
|
40
65
|
requirements: verifiedStreamRequirementsSchema,
|
|
41
66
|
});
|
|
42
67
|
|
|
43
|
-
export interface
|
|
44
|
-
typeof
|
|
68
|
+
export interface StreamChallenge extends z.infer<
|
|
69
|
+
typeof streamChallengeSchema
|
|
45
70
|
> {}
|
|
46
71
|
|
|
47
|
-
export const
|
|
48
|
-
challenge:
|
|
72
|
+
export const streamOpenPayloadSchema = z.object({
|
|
73
|
+
challenge: streamChallengeSchema,
|
|
49
74
|
signedMandate: signedSpendMandateSchema,
|
|
50
75
|
});
|
|
51
76
|
|
|
52
|
-
export interface
|
|
53
|
-
typeof
|
|
77
|
+
export interface StreamOpenPayload extends z.infer<
|
|
78
|
+
typeof streamOpenPayloadSchema
|
|
54
79
|
> {}
|
|
55
80
|
|
|
56
|
-
export interface
|
|
81
|
+
export interface StreamFacilitator {
|
|
57
82
|
/** Build the 402 challenge a server returns to prompt a verified stream. */
|
|
58
|
-
challenge(req: VerifiedStreamRequirements):
|
|
83
|
+
challenge(req: VerifiedStreamRequirements): StreamChallenge;
|
|
59
84
|
/** Verify + open the stream from an agent's signed payment payload. */
|
|
60
85
|
open(payloadHeader: string): Promise<{ streamId: `0x${string}` }>;
|
|
61
86
|
/** Prove the HTTPS response consumed after opening a verified stream. */
|
|
62
87
|
proveConsumedHttpsResponse(
|
|
63
|
-
input:
|
|
88
|
+
input: ConsumedHttpsResponseProofInput,
|
|
64
89
|
): Promise<VerifiedConsumedHttpsResponseProof>;
|
|
65
90
|
}
|
|
66
91
|
|
|
67
|
-
export interface
|
|
92
|
+
export interface ConsumedHttpsResponseProofInput {
|
|
68
93
|
readonly signedMandate: SignedSpendMandate;
|
|
69
94
|
readonly streamId?: `0x${string}`;
|
|
70
95
|
readonly operator?: `0x${string}`;
|
|
@@ -109,9 +134,11 @@ function warnFacilitatorOnce(key: string, message: string): void {
|
|
|
109
134
|
console.error(message);
|
|
110
135
|
}
|
|
111
136
|
|
|
112
|
-
export class
|
|
137
|
+
export class StreamPayloadError extends Error {
|
|
113
138
|
constructor(message: string) {
|
|
114
139
|
super(message);
|
|
140
|
+
// Unchanged on purpose: `error.name` is an observable value that existing
|
|
141
|
+
// consumers may match on. The class was renamed; its `name` was not.
|
|
115
142
|
this.name = "X402PayloadError";
|
|
116
143
|
}
|
|
117
144
|
}
|
|
@@ -137,14 +164,15 @@ export class DeliveryProofUnavailableError extends Error {
|
|
|
137
164
|
* facilitator denies the open in this case (fail-closed, buyer-favoring) rather
|
|
138
165
|
* than proceeding on an unverified assumption of "0 spent / none revoked".
|
|
139
166
|
*/
|
|
140
|
-
export class
|
|
167
|
+
export class SpendControlUnavailableError extends Error {
|
|
141
168
|
constructor(message: string) {
|
|
142
169
|
super(message);
|
|
170
|
+
// Unchanged on purpose: see StreamPayloadError above.
|
|
143
171
|
this.name = "X402SpendControlUnavailableError";
|
|
144
172
|
}
|
|
145
173
|
}
|
|
146
174
|
|
|
147
|
-
export class
|
|
175
|
+
export class VerifiedStreamFacilitator implements StreamFacilitator {
|
|
148
176
|
private readonly agentClient: VerifiedStreamAgentOpener;
|
|
149
177
|
private readonly deliveryProofs: ConsumerDeliveryProofService | undefined;
|
|
150
178
|
private readonly sdkConfig: StreamProofClientConfig;
|
|
@@ -181,26 +209,26 @@ export class VerifiedStreamX402Facilitator implements X402Facilitator {
|
|
|
181
209
|
// both resolvers are wired; warn once so the reason is visible at deploy time.
|
|
182
210
|
if (options.resolveSpentSoFarUsdc === undefined) {
|
|
183
211
|
warnFacilitatorOnce(
|
|
184
|
-
"
|
|
185
|
-
"[metrik]
|
|
212
|
+
"metrik-stream-spent",
|
|
213
|
+
"[metrik] Metrik verified-stream facilitator: no resolveSpentSoFarUsdc wired — the mandate cumulative cap (maxTotalUsdc) cannot be verified, so open() will DENY every spend (fail-closed) until a resolver is wired.",
|
|
186
214
|
);
|
|
187
215
|
}
|
|
188
216
|
if (options.resolveRevokedMandateIds === undefined) {
|
|
189
217
|
warnFacilitatorOnce(
|
|
190
|
-
"
|
|
191
|
-
"[metrik]
|
|
218
|
+
"metrik-stream-revocation",
|
|
219
|
+
"[metrik] Metrik verified-stream facilitator: no resolveRevokedMandateIds wired — revocation state cannot be verified, so open() will DENY every spend (fail-closed) until a resolver is wired.",
|
|
192
220
|
);
|
|
193
221
|
}
|
|
194
222
|
}
|
|
195
223
|
|
|
196
|
-
challenge(req: VerifiedStreamRequirements):
|
|
224
|
+
challenge(req: VerifiedStreamRequirements): StreamChallenge {
|
|
197
225
|
const requirements = verifiedStreamRequirementsSchema.parse(req);
|
|
198
226
|
assertSettlementTarget(requirements, this.sdkConfig);
|
|
199
|
-
return { scheme:
|
|
227
|
+
return { scheme: METRIK_STREAM_SCHEME, requirements };
|
|
200
228
|
}
|
|
201
229
|
|
|
202
230
|
async open(payloadHeader: string): Promise<{ streamId: `0x${string}` }> {
|
|
203
|
-
const payload =
|
|
231
|
+
const payload = parseStreamPayloadHeader(payloadHeader);
|
|
204
232
|
assertSettlementTarget(payload.challenge.requirements, this.sdkConfig);
|
|
205
233
|
|
|
206
234
|
// FAIL-CLOSED fund safety (B6). Both the cumulative-cap accounting and the
|
|
@@ -209,12 +237,12 @@ export class VerifiedStreamX402Facilitator implements X402Facilitator {
|
|
|
209
237
|
// returns a non-bigint/negative spend is treated as "state cannot be
|
|
210
238
|
// verified" and DENIES the open — never a silent fail-open to 0/none.
|
|
211
239
|
if (this.resolveSpentSoFarUsdc === undefined) {
|
|
212
|
-
throw new
|
|
240
|
+
throw new SpendControlUnavailableError(
|
|
213
241
|
"cannot open verified stream: no resolveSpentSoFarUsdc wired — the mandate cumulative cap (maxTotalUsdc) cannot be verified (fail-closed)",
|
|
214
242
|
);
|
|
215
243
|
}
|
|
216
244
|
if (this.resolveRevokedMandateIds === undefined) {
|
|
217
|
-
throw new
|
|
245
|
+
throw new SpendControlUnavailableError(
|
|
218
246
|
"cannot open verified stream: no resolveRevokedMandateIds wired — mandate revocation state cannot be verified (fail-closed)",
|
|
219
247
|
);
|
|
220
248
|
}
|
|
@@ -223,14 +251,14 @@ export class VerifiedStreamX402Facilitator implements X402Facilitator {
|
|
|
223
251
|
try {
|
|
224
252
|
spentSoFarUsdc = await this.resolveSpentSoFarUsdc(payload.signedMandate);
|
|
225
253
|
} catch (error) {
|
|
226
|
-
throw new
|
|
254
|
+
throw new SpendControlUnavailableError(
|
|
227
255
|
`cannot verify mandate cumulative cap (maxTotalUsdc): ${
|
|
228
256
|
error instanceof Error ? error.message : String(error)
|
|
229
257
|
} (fail-closed)`,
|
|
230
258
|
);
|
|
231
259
|
}
|
|
232
260
|
if (typeof spentSoFarUsdc !== "bigint" || spentSoFarUsdc < 0n) {
|
|
233
|
-
throw new
|
|
261
|
+
throw new SpendControlUnavailableError(
|
|
234
262
|
"cannot verify mandate cumulative cap (maxTotalUsdc): resolver returned an invalid spent-so-far value (fail-closed)",
|
|
235
263
|
);
|
|
236
264
|
}
|
|
@@ -241,7 +269,7 @@ export class VerifiedStreamX402Facilitator implements X402Facilitator {
|
|
|
241
269
|
payload.signedMandate,
|
|
242
270
|
);
|
|
243
271
|
} catch (error) {
|
|
244
|
-
throw new
|
|
272
|
+
throw new SpendControlUnavailableError(
|
|
245
273
|
`cannot verify mandate revocation state: ${
|
|
246
274
|
error instanceof Error ? error.message : String(error)
|
|
247
275
|
} (fail-closed)`,
|
|
@@ -254,7 +282,7 @@ export class VerifiedStreamX402Facilitator implements X402Facilitator {
|
|
|
254
282
|
Symbol.iterator
|
|
255
283
|
] !== "function"
|
|
256
284
|
) {
|
|
257
|
-
throw new
|
|
285
|
+
throw new SpendControlUnavailableError(
|
|
258
286
|
"cannot verify mandate revocation state: resolver returned a non-iterable value (fail-closed)",
|
|
259
287
|
);
|
|
260
288
|
}
|
|
@@ -275,11 +303,11 @@ export class VerifiedStreamX402Facilitator implements X402Facilitator {
|
|
|
275
303
|
}
|
|
276
304
|
|
|
277
305
|
async proveConsumedHttpsResponse(
|
|
278
|
-
input:
|
|
306
|
+
input: ConsumedHttpsResponseProofInput,
|
|
279
307
|
): Promise<VerifiedConsumedHttpsResponseProof> {
|
|
280
308
|
if (this.deliveryProofs === undefined) {
|
|
281
309
|
throw new DeliveryProofUnavailableError(
|
|
282
|
-
"zkTLS delivery proofs are not configured on this
|
|
310
|
+
"zkTLS delivery proofs are not configured on this Metrik verified-stream facilitator",
|
|
283
311
|
);
|
|
284
312
|
}
|
|
285
313
|
|
|
@@ -317,42 +345,52 @@ export class VerifiedStreamX402Facilitator implements X402Facilitator {
|
|
|
317
345
|
}
|
|
318
346
|
}
|
|
319
347
|
|
|
320
|
-
export function
|
|
321
|
-
const parsed =
|
|
322
|
-
const token = Buffer.from(
|
|
323
|
-
|
|
348
|
+
export function encodeStreamPayloadHeader(payload: StreamOpenPayload): string {
|
|
349
|
+
const parsed = streamOpenPayloadSchema.parse(payload);
|
|
350
|
+
const token = Buffer.from(stringifyStreamPayload(parsed)).toString(
|
|
351
|
+
"base64url",
|
|
352
|
+
);
|
|
353
|
+
return `${METRIK_STREAM_SCHEME} ${token}`;
|
|
324
354
|
}
|
|
325
355
|
|
|
326
|
-
export function
|
|
327
|
-
const parsed =
|
|
356
|
+
export function parseStreamChallengeJson(value: unknown): StreamChallenge {
|
|
357
|
+
const parsed = streamChallengeSchema.safeParse(reviveBigInts(value));
|
|
328
358
|
if (!parsed.success) {
|
|
329
|
-
throw new
|
|
359
|
+
throw new StreamPayloadError(
|
|
330
360
|
parsed.error.issues.map((issue) => issue.message).join("; "),
|
|
331
361
|
);
|
|
332
362
|
}
|
|
333
363
|
return parsed.data;
|
|
334
364
|
}
|
|
335
365
|
|
|
336
|
-
export function
|
|
366
|
+
export function parseStreamPayloadHeader(
|
|
367
|
+
payloadHeader: string,
|
|
368
|
+
): StreamOpenPayload {
|
|
337
369
|
const [scheme, token] = payloadHeader.split(" ", 2);
|
|
338
|
-
if (
|
|
339
|
-
|
|
370
|
+
if (
|
|
371
|
+
scheme !== METRIK_STREAM_SCHEME ||
|
|
372
|
+
token === undefined ||
|
|
373
|
+
token.trim() === ""
|
|
374
|
+
) {
|
|
375
|
+
throw new StreamPayloadError(
|
|
376
|
+
"invalid Metrik verified-stream payload header",
|
|
377
|
+
);
|
|
340
378
|
}
|
|
341
379
|
|
|
342
380
|
let decoded: unknown;
|
|
343
381
|
try {
|
|
344
382
|
decoded = JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
|
|
345
383
|
} catch (error) {
|
|
346
|
-
throw new
|
|
347
|
-
`invalid
|
|
384
|
+
throw new StreamPayloadError(
|
|
385
|
+
`invalid Metrik verified-stream payload encoding: ${
|
|
348
386
|
error instanceof Error ? error.message : String(error)
|
|
349
387
|
}`,
|
|
350
388
|
);
|
|
351
389
|
}
|
|
352
390
|
|
|
353
|
-
const parsed =
|
|
391
|
+
const parsed = streamOpenPayloadSchema.safeParse(reviveBigInts(decoded));
|
|
354
392
|
if (!parsed.success) {
|
|
355
|
-
throw new
|
|
393
|
+
throw new StreamPayloadError(
|
|
356
394
|
parsed.error.issues.map((issue) => issue.message).join("; "),
|
|
357
395
|
);
|
|
358
396
|
}
|
|
@@ -381,7 +419,7 @@ function assertSettlementTarget(
|
|
|
381
419
|
}
|
|
382
420
|
}
|
|
383
421
|
|
|
384
|
-
function
|
|
422
|
+
function stringifyStreamPayload(payload: StreamOpenPayload): string {
|
|
385
423
|
return JSON.stringify(payload, (_key, value) =>
|
|
386
424
|
typeof value === "bigint" ? `${value.toString()}n` : value,
|
|
387
425
|
);
|
|
@@ -404,3 +442,44 @@ function reviveBigInts(value: unknown): unknown {
|
|
|
404
442
|
|
|
405
443
|
return value;
|
|
406
444
|
}
|
|
445
|
+
|
|
446
|
+
/* -------------------------------------------------------------------------- */
|
|
447
|
+
/* Deprecated `x402`-flavoured aliases. */
|
|
448
|
+
/* */
|
|
449
|
+
/* These names are misleading — this flow is Metrik-native and not x402 (see */
|
|
450
|
+
/* `docs/x402.md`). They are retained as aliases bound to the SAME values and */
|
|
451
|
+
/* class objects as the canonical exports above, so existing consumers and */
|
|
452
|
+
/* `instanceof` checks keep working. Prefer the canonical names. */
|
|
453
|
+
/* -------------------------------------------------------------------------- */
|
|
454
|
+
|
|
455
|
+
export {
|
|
456
|
+
/** @deprecated Use {@link METRIK_STREAM_SCHEME}. This flow is not x402. */
|
|
457
|
+
METRIK_STREAM_SCHEME as X402_SCHEME,
|
|
458
|
+
/** @deprecated Use {@link VerifiedStreamFacilitator}. This flow is not x402. */
|
|
459
|
+
VerifiedStreamFacilitator as VerifiedStreamX402Facilitator,
|
|
460
|
+
/** @deprecated Use {@link StreamPayloadError}. Same class object. */
|
|
461
|
+
StreamPayloadError as X402PayloadError,
|
|
462
|
+
/** @deprecated Use {@link SpendControlUnavailableError}. Same class object. */
|
|
463
|
+
SpendControlUnavailableError as X402SpendControlUnavailableError,
|
|
464
|
+
/** @deprecated Use {@link streamChallengeSchema}. */
|
|
465
|
+
streamChallengeSchema as x402StreamChallengeSchema,
|
|
466
|
+
/** @deprecated Use {@link streamOpenPayloadSchema}. */
|
|
467
|
+
streamOpenPayloadSchema as x402OpenPayloadSchema,
|
|
468
|
+
/** @deprecated Use {@link encodeStreamPayloadHeader}. */
|
|
469
|
+
encodeStreamPayloadHeader as encodeX402PayloadHeader,
|
|
470
|
+
/** @deprecated Use {@link parseStreamPayloadHeader}. */
|
|
471
|
+
parseStreamPayloadHeader as parseX402PayloadHeader,
|
|
472
|
+
/** @deprecated Use {@link parseStreamChallengeJson}. */
|
|
473
|
+
parseStreamChallengeJson as parseX402ChallengeJson,
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
export type {
|
|
477
|
+
/** @deprecated Use {@link StreamFacilitator}. This flow is not x402. */
|
|
478
|
+
StreamFacilitator as X402Facilitator,
|
|
479
|
+
/** @deprecated Use {@link StreamChallenge}. */
|
|
480
|
+
StreamChallenge as X402StreamChallenge,
|
|
481
|
+
/** @deprecated Use {@link StreamOpenPayload}. */
|
|
482
|
+
StreamOpenPayload as X402OpenPayload,
|
|
483
|
+
/** @deprecated Use {@link ConsumedHttpsResponseProofInput}. */
|
|
484
|
+
ConsumedHttpsResponseProofInput as X402ConsumedHttpsResponseProofInput,
|
|
485
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { Address, Hex } from "viem";
|
|
2
|
+
import { getAddress, isAddress, isHex } from "viem";
|
|
3
|
+
// TYPE-ONLY import: erased at compile time, so the core package never gains a
|
|
4
|
+
// runtime dependency on `x402`. The shape is still pinned to the real library —
|
|
5
|
+
// `test/x402-facilitator-conformance.test.ts` parses what this module builds
|
|
6
|
+
// with the REAL `PaymentRequirementsSchema`, so a drift in either direction
|
|
7
|
+
// fails CI rather than shipping.
|
|
8
|
+
import type { PaymentRequirements } from "x402/types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Standard x402 `exact` payment requirements, built from an OPERATOR-SIGNED
|
|
12
|
+
* Metrik listing.
|
|
13
|
+
*
|
|
14
|
+
* This is the "additional way in" described in `docs/x402.md`: the Metrik
|
|
15
|
+
* verified-stream flow (`./facilitator.ts`) is unchanged and is still the only
|
|
16
|
+
* path with recoverable escrow. What this module adds is the ability for a
|
|
17
|
+
* seller's gateway to ALSO quote an ordinary, fully conformant x402 `exact`
|
|
18
|
+
* price for a single call, so an agent holding nothing but `x402-fetch` can pay
|
|
19
|
+
* without knowing Metrik exists.
|
|
20
|
+
*
|
|
21
|
+
* ## What is signed and what is not
|
|
22
|
+
*
|
|
23
|
+
* `payTo` is derived from the operator's EIP-712-signed service descriptor and
|
|
24
|
+
* from nothing else. It is never read from configuration and never read from a
|
|
25
|
+
* caller-supplied field, because `payTo` is the one field whose corruption
|
|
26
|
+
* silently redirects money.
|
|
27
|
+
*
|
|
28
|
+
* The PRICE is not signed. The descriptor schema (`@absol-labs/shared`) carries
|
|
29
|
+
* `minRatePerSecond` / `maxRatePerSecond` for metered streams and has no
|
|
30
|
+
* per-call price field, so a per-call x402 amount can only come from the seller
|
|
31
|
+
* running the gateway. That is legitimate — it is the seller's own price on the
|
|
32
|
+
* seller's own gateway — but a buyer CANNOT cross-check it against the registry
|
|
33
|
+
* the way they can cross-check `payTo`. Said plainly here so nobody assumes a
|
|
34
|
+
* guarantee that does not exist.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** The only x402 protocol version implemented by the published `x402` package. */
|
|
38
|
+
export const X402_VERSION = 1 as const;
|
|
39
|
+
|
|
40
|
+
/** The only x402 payment scheme implemented by the published `x402` package. */
|
|
41
|
+
export const X402_EXACT_SCHEME = "exact" as const;
|
|
42
|
+
|
|
43
|
+
/** x402 v1 uses bare network names, not CAIP-2. Metrik settles on Base only. */
|
|
44
|
+
export const X402_NETWORK_BY_CHAIN_ID: Readonly<
|
|
45
|
+
Record<number, "base" | "base-sepolia">
|
|
46
|
+
> = Object.freeze({
|
|
47
|
+
8453: "base",
|
|
48
|
+
84532: "base-sepolia",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export interface X402AssetConfig {
|
|
52
|
+
readonly address: Address;
|
|
53
|
+
/** EIP-712 domain `name` of the asset, needed to sign EIP-3009 offline. */
|
|
54
|
+
readonly eip712Name: string;
|
|
55
|
+
/** EIP-712 domain `version` of the asset. */
|
|
56
|
+
readonly eip712Version: string;
|
|
57
|
+
readonly decimals: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* USDC per Base chain. The EIP-712 domain values are what a client needs to
|
|
62
|
+
* sign `TransferWithAuthorization` WITHOUT an extra RPC round trip: the x402
|
|
63
|
+
* client falls back to reading `version()` on chain only when
|
|
64
|
+
* `PaymentRequirements.extra` omits it, so quoting them here removes a network
|
|
65
|
+
* dependency from the payer's hot path.
|
|
66
|
+
*
|
|
67
|
+
* Base Sepolia values read from the live contract on 2026-09-18
|
|
68
|
+
* (`name() == "USDC"`, `version() == "2"`).
|
|
69
|
+
*/
|
|
70
|
+
export const X402_USDC_BY_CHAIN_ID: Readonly<Record<number, X402AssetConfig>> =
|
|
71
|
+
Object.freeze({
|
|
72
|
+
8453: Object.freeze({
|
|
73
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as Address,
|
|
74
|
+
eip712Name: "USD Coin",
|
|
75
|
+
eip712Version: "2",
|
|
76
|
+
decimals: 6,
|
|
77
|
+
}),
|
|
78
|
+
84532: Object.freeze({
|
|
79
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as Address,
|
|
80
|
+
eip712Name: "USDC",
|
|
81
|
+
eip712Version: "2",
|
|
82
|
+
decimals: 6,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export class X402RequirementsError extends Error {
|
|
87
|
+
constructor(message: string) {
|
|
88
|
+
super(message);
|
|
89
|
+
this.name = "X402RequirementsError";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The minimum a payee must prove. Both fields MUST be projected from the
|
|
95
|
+
* operator-signed descriptor (see `../discovery/registry.ts`, which recovers the
|
|
96
|
+
* EIP-712 signer and re-derives `serviceRef` from the signed content), never
|
|
97
|
+
* from an unsigned registry column and never from a request.
|
|
98
|
+
*/
|
|
99
|
+
export interface SignedListingPayee {
|
|
100
|
+
readonly serviceRef: Hex;
|
|
101
|
+
readonly operator: Address;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface BuildMetrikPaymentRequirementsInput {
|
|
105
|
+
readonly listing: SignedListingPayee;
|
|
106
|
+
/** Absolute URL of the paid resource, as the buyer will request it. */
|
|
107
|
+
readonly resource: string;
|
|
108
|
+
/** Price for ONE call, in the asset's base units (micro-USDC at 6 decimals). */
|
|
109
|
+
readonly priceBaseUnits: bigint;
|
|
110
|
+
readonly chainId: number;
|
|
111
|
+
readonly description?: string;
|
|
112
|
+
readonly mimeType?: string;
|
|
113
|
+
/** How long the signed authorization stays valid. Default 60s. */
|
|
114
|
+
readonly maxTimeoutSeconds?: number;
|
|
115
|
+
readonly outputSchema?: Record<string, unknown>;
|
|
116
|
+
/** Override the settlement asset (defaults to USDC for `chainId`). */
|
|
117
|
+
readonly asset?: X402AssetConfig;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Default authorization validity window quoted to payers, in seconds. */
|
|
121
|
+
export const DEFAULT_X402_MAX_TIMEOUT_SECONDS = 60;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Builds standard-`exact` `PaymentRequirements` for one call of a Metrik-listed
|
|
125
|
+
* resource. Throws rather than emitting anything a conformant client would
|
|
126
|
+
* reject or a payer could be harmed by.
|
|
127
|
+
*/
|
|
128
|
+
export function buildMetrikPaymentRequirements(
|
|
129
|
+
input: BuildMetrikPaymentRequirementsInput,
|
|
130
|
+
): PaymentRequirements {
|
|
131
|
+
const network = X402_NETWORK_BY_CHAIN_ID[input.chainId];
|
|
132
|
+
if (network === undefined) {
|
|
133
|
+
throw new X402RequirementsError(
|
|
134
|
+
`x402 v1 has no network name for chain ${input.chainId} (Metrik settles on Base: 8453, 84532)`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const asset = input.asset ?? X402_USDC_BY_CHAIN_ID[input.chainId];
|
|
138
|
+
if (asset === undefined) {
|
|
139
|
+
throw new X402RequirementsError(
|
|
140
|
+
`no default settlement asset known for chain ${input.chainId}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!isAddress(input.listing.operator)) {
|
|
144
|
+
throw new X402RequirementsError(
|
|
145
|
+
"listing.operator must be an EVM address (it becomes payTo)",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
!isHex(input.listing.serviceRef, { strict: true }) ||
|
|
150
|
+
input.listing.serviceRef.length !== 66
|
|
151
|
+
) {
|
|
152
|
+
throw new X402RequirementsError(
|
|
153
|
+
"listing.serviceRef must be a 32-byte hex value",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (input.priceBaseUnits <= 0n) {
|
|
157
|
+
throw new X402RequirementsError(
|
|
158
|
+
"priceBaseUnits must be positive — a zero-priced x402 quote is not a payment",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
let resource: URL;
|
|
162
|
+
try {
|
|
163
|
+
resource = new URL(input.resource);
|
|
164
|
+
} catch {
|
|
165
|
+
throw new X402RequirementsError(`resource must be an absolute URL`);
|
|
166
|
+
}
|
|
167
|
+
const maxTimeoutSeconds =
|
|
168
|
+
input.maxTimeoutSeconds ?? DEFAULT_X402_MAX_TIMEOUT_SECONDS;
|
|
169
|
+
if (!Number.isInteger(maxTimeoutSeconds) || maxTimeoutSeconds <= 0) {
|
|
170
|
+
throw new X402RequirementsError(
|
|
171
|
+
"maxTimeoutSeconds must be a positive integer",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
scheme: X402_EXACT_SCHEME,
|
|
177
|
+
network,
|
|
178
|
+
maxAmountRequired: input.priceBaseUnits.toString(),
|
|
179
|
+
resource: resource.toString(),
|
|
180
|
+
description:
|
|
181
|
+
input.description ??
|
|
182
|
+
"One call to a Metrik-listed service. Metrik's facilitator refuses to settle this payment while its oracle reports the service as failing.",
|
|
183
|
+
mimeType: input.mimeType ?? "application/json",
|
|
184
|
+
payTo: getAddress(input.listing.operator),
|
|
185
|
+
maxTimeoutSeconds,
|
|
186
|
+
asset: getAddress(asset.address),
|
|
187
|
+
extra: {
|
|
188
|
+
// Required by the x402 `exact` EVM binding so the payer can sign the
|
|
189
|
+
// EIP-3009 authorization offline.
|
|
190
|
+
name: asset.eip712Name,
|
|
191
|
+
version: asset.eip712Version,
|
|
192
|
+
// ADVISORY ONLY. The facilitator NEVER reads this to decide anything: it
|
|
193
|
+
// arrives inside a caller-supplied request body, so trusting it would let
|
|
194
|
+
// a caller pick which service's delivery record gets checked. The
|
|
195
|
+
// facilitator re-derives the service from `resource` against its own
|
|
196
|
+
// registry view. Published purely so a client can look the listing up.
|
|
197
|
+
metrikServiceRef: input.listing.serviceRef,
|
|
198
|
+
},
|
|
199
|
+
...(input.outputSchema === undefined
|
|
200
|
+
? {}
|
|
201
|
+
: { outputSchema: input.outputSchema }),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The HTTP `402` response body a resource server returns.
|
|
207
|
+
*
|
|
208
|
+
* NOTE on `error`: the shipped `x402ResponseSchema` types `error` as the closed
|
|
209
|
+
* `ErrorReasons` enum, which no real x402 server obeys — `x402-express` and
|
|
210
|
+
* friends put human-readable text there, and `x402-fetch` never reads the field
|
|
211
|
+
* at all. Metrik follows the real-world behaviour (free text) rather than the
|
|
212
|
+
* schema; `test/x402-facilitator-conformance.test.ts` pins that divergence so it
|
|
213
|
+
* is a recorded decision and not an accident.
|
|
214
|
+
*/
|
|
215
|
+
export interface X402PaymentRequiredBody {
|
|
216
|
+
readonly x402Version: typeof X402_VERSION;
|
|
217
|
+
readonly error: string;
|
|
218
|
+
readonly accepts: readonly PaymentRequirements[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function buildX402PaymentRequiredBody(
|
|
222
|
+
accepts: readonly PaymentRequirements[],
|
|
223
|
+
error = "X-PAYMENT header is required",
|
|
224
|
+
): X402PaymentRequiredBody {
|
|
225
|
+
if (accepts.length === 0) {
|
|
226
|
+
throw new X402RequirementsError(
|
|
227
|
+
"a 402 must advertise at least one PaymentRequirements",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return { x402Version: X402_VERSION, error, accepts };
|
|
231
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPublicClient,
|
|
3
|
+
createWalletClient,
|
|
4
|
+
http,
|
|
5
|
+
publicActions,
|
|
6
|
+
type Chain,
|
|
7
|
+
type Hex,
|
|
8
|
+
} from "viem";
|
|
9
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
10
|
+
import { base, baseSepolia } from "viem/chains";
|
|
11
|
+
import { settle as x402Settle, verify as x402Verify } from "x402/facilitator";
|
|
12
|
+
import type {
|
|
13
|
+
PaymentPayload,
|
|
14
|
+
PaymentRequirements,
|
|
15
|
+
SettleResponse,
|
|
16
|
+
VerifyResponse,
|
|
17
|
+
} from "x402/types";
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
PaymentSettler,
|
|
21
|
+
PaymentVerifier,
|
|
22
|
+
} from "./verified-delivery-facilitator.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Chain wiring for the payment half of Metrik's x402 facilitator.
|
|
26
|
+
*
|
|
27
|
+
* Nothing here is Metrik-specific: verification and settlement are delegated
|
|
28
|
+
* VERBATIM to the real `x402/facilitator` implementation of the `exact` scheme.
|
|
29
|
+
* Metrik does not reimplement EIP-3009, and a change in the library's rules is
|
|
30
|
+
* inherited rather than drifted away from. The Metrik-specific part — the
|
|
31
|
+
* delivery gate — lives entirely in `./verified-delivery-facilitator.ts` and is
|
|
32
|
+
* layered ON TOP of whatever this returns.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export const X402_CHAINS: Readonly<Record<number, Chain>> = Object.freeze({
|
|
36
|
+
8453: base,
|
|
37
|
+
84532: baseSepolia,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export interface X402ChainOptions {
|
|
41
|
+
/** Default `84532` (Base Sepolia). */
|
|
42
|
+
readonly chainId?: number;
|
|
43
|
+
/** RPC endpoint. Required: the library's default public RPCs are not operable infrastructure. */
|
|
44
|
+
readonly rpcUrl: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function chainFor(chainId: number): Chain {
|
|
48
|
+
const chain = X402_CHAINS[chainId];
|
|
49
|
+
if (chain === undefined) {
|
|
50
|
+
throw new Error(`unsupported x402 settlement chain: ${chainId}`);
|
|
51
|
+
}
|
|
52
|
+
return chain;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read-only verifier. Needs an RPC because the `exact` scheme checks the payer's
|
|
57
|
+
* on-chain balance; it holds no key and can broadcast nothing.
|
|
58
|
+
*/
|
|
59
|
+
export function createX402PaymentVerifier(
|
|
60
|
+
options: X402ChainOptions,
|
|
61
|
+
): PaymentVerifier {
|
|
62
|
+
const chain = chainFor(options.chainId ?? 84532);
|
|
63
|
+
const client = createPublicClient({
|
|
64
|
+
chain,
|
|
65
|
+
transport: http(options.rpcUrl),
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
verify(
|
|
69
|
+
payload: PaymentPayload,
|
|
70
|
+
requirements: PaymentRequirements,
|
|
71
|
+
): Promise<VerifyResponse> {
|
|
72
|
+
return x402Verify(
|
|
73
|
+
client as Parameters<typeof x402Verify>[0],
|
|
74
|
+
payload,
|
|
75
|
+
requirements,
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface X402RelayerOptions extends X402ChainOptions {
|
|
82
|
+
/**
|
|
83
|
+
* The relayer's private key. This wallet BROADCASTS and pays gas; it never
|
|
84
|
+
* custodies payment funds (EIP-3009 moves value directly from payer to payee).
|
|
85
|
+
* Supplied by the operator through the environment, exactly like every other
|
|
86
|
+
* wallet in this package. Never committed, never defaulted.
|
|
87
|
+
*/
|
|
88
|
+
readonly privateKey: Hex;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The settlement half. Constructing this is the ONLY place a key enters the
|
|
93
|
+
* facilitator, and there is no code path that broadcasts without one.
|
|
94
|
+
*/
|
|
95
|
+
export function createX402PaymentSettler(
|
|
96
|
+
options: X402RelayerOptions,
|
|
97
|
+
): PaymentSettler {
|
|
98
|
+
const chain = chainFor(options.chainId ?? 84532);
|
|
99
|
+
const account = privateKeyToAccount(options.privateKey);
|
|
100
|
+
const signer = createWalletClient({
|
|
101
|
+
account,
|
|
102
|
+
chain,
|
|
103
|
+
transport: http(options.rpcUrl),
|
|
104
|
+
}).extend(publicActions);
|
|
105
|
+
return {
|
|
106
|
+
settle(
|
|
107
|
+
payload: PaymentPayload,
|
|
108
|
+
requirements: PaymentRequirements,
|
|
109
|
+
): Promise<SettleResponse> {
|
|
110
|
+
return x402Settle(
|
|
111
|
+
signer as Parameters<typeof x402Settle>[0],
|
|
112
|
+
payload,
|
|
113
|
+
requirements,
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The relayer address, so an operator can check what they must fund with gas. */
|
|
120
|
+
export function relayerAddress(privateKey: Hex): `0x${string}` {
|
|
121
|
+
return privateKeyToAccount(privateKey).address;
|
|
122
|
+
}
|