@absol-labs/agent 0.10.0 → 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.
Files changed (141) hide show
  1. package/README.md +107 -23
  2. package/dist/capability/invocation-capability.d.ts +18 -6
  3. package/dist/capability/invocation-capability.d.ts.map +1 -1
  4. package/dist/capability/invocation-capability.js.map +1 -1
  5. package/dist/cli/commands/claim.d.ts.map +1 -1
  6. package/dist/cli/commands/claim.js +20 -4
  7. package/dist/cli/commands/claim.js.map +1 -1
  8. package/dist/cli/commands/close.d.ts.map +1 -1
  9. package/dist/cli/commands/close.js +16 -4
  10. package/dist/cli/commands/close.js.map +1 -1
  11. package/dist/cli/commands/invoke.d.ts.map +1 -1
  12. package/dist/cli/commands/invoke.js +1 -0
  13. package/dist/cli/commands/invoke.js.map +1 -1
  14. package/dist/cli/commands/open.d.ts.map +1 -1
  15. package/dist/cli/commands/open.js +63 -11
  16. package/dist/cli/commands/open.js.map +1 -1
  17. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
  18. package/dist/cli/commands/reclaim-unverified.js +12 -4
  19. package/dist/cli/commands/reclaim-unverified.js.map +1 -1
  20. package/dist/cli/commands/reclaim.d.ts.map +1 -1
  21. package/dist/cli/commands/reclaim.js +16 -5
  22. package/dist/cli/commands/reclaim.js.map +1 -1
  23. package/dist/cli/commands/streams.js +1 -1
  24. package/dist/cli/commands/streams.js.map +1 -1
  25. package/dist/cli/deps.d.ts +53 -7
  26. package/dist/cli/deps.d.ts.map +1 -1
  27. package/dist/cli/deps.js +65 -15
  28. package/dist/cli/deps.js.map +1 -1
  29. package/dist/cli/privy.d.ts +83 -0
  30. package/dist/cli/privy.d.ts.map +1 -0
  31. package/dist/cli/privy.js +230 -0
  32. package/dist/cli/privy.js.map +1 -0
  33. package/dist/cli/spend-policy.d.ts +60 -0
  34. package/dist/cli/spend-policy.d.ts.map +1 -0
  35. package/dist/cli/spend-policy.js +65 -0
  36. package/dist/cli/spend-policy.js.map +1 -0
  37. package/dist/cli/wallet.d.ts +108 -39
  38. package/dist/cli/wallet.d.ts.map +1 -1
  39. package/dist/cli/wallet.js +108 -43
  40. package/dist/cli/wallet.js.map +1 -1
  41. package/dist/gateway/caller-auth-gateway.d.ts +38 -3
  42. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  43. package/dist/gateway/caller-auth-gateway.js +25 -1
  44. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  45. package/dist/gateway/http-server.d.ts +53 -0
  46. package/dist/gateway/http-server.d.ts.map +1 -1
  47. package/dist/gateway/http-server.js +189 -4
  48. package/dist/gateway/http-server.js.map +1 -1
  49. package/dist/gateway/x402-mode.d.ts +123 -0
  50. package/dist/gateway/x402-mode.d.ts.map +1 -0
  51. package/dist/gateway/x402-mode.js +212 -0
  52. package/dist/gateway/x402-mode.js.map +1 -0
  53. package/dist/index.d.ts +6 -4
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +8 -3
  56. package/dist/index.js.map +1 -1
  57. package/dist/mandates/env.d.ts +3 -2
  58. package/dist/mandates/env.d.ts.map +1 -1
  59. package/dist/mandates/env.js +3 -2
  60. package/dist/mandates/env.js.map +1 -1
  61. package/dist/mcp/server.d.ts +49 -0
  62. package/dist/mcp/server.d.ts.map +1 -1
  63. package/dist/mcp/server.js +513 -24
  64. package/dist/mcp/server.js.map +1 -1
  65. package/dist/mcp/service-invoker.d.ts +97 -0
  66. package/dist/mcp/service-invoker.d.ts.map +1 -0
  67. package/dist/mcp/service-invoker.js +167 -0
  68. package/dist/mcp/service-invoker.js.map +1 -0
  69. package/dist/sdk/invoke.d.ts +19 -4
  70. package/dist/sdk/invoke.d.ts.map +1 -1
  71. package/dist/sdk/invoke.js +69 -5
  72. package/dist/sdk/invoke.js.map +1 -1
  73. package/dist/x402/delivery-oracle.d.ts +108 -0
  74. package/dist/x402/delivery-oracle.d.ts.map +1 -0
  75. package/dist/x402/delivery-oracle.js +174 -0
  76. package/dist/x402/delivery-oracle.js.map +1 -0
  77. package/dist/x402/facilitator-server-entry.d.ts +3 -0
  78. package/dist/x402/facilitator-server-entry.d.ts.map +1 -0
  79. package/dist/x402/facilitator-server-entry.js +27 -0
  80. package/dist/x402/facilitator-server-entry.js.map +1 -0
  81. package/dist/x402/facilitator-server.d.ts +73 -0
  82. package/dist/x402/facilitator-server.d.ts.map +1 -0
  83. package/dist/x402/facilitator-server.js +305 -0
  84. package/dist/x402/facilitator-server.js.map +1 -0
  85. package/dist/x402/facilitator.d.ts +69 -17
  86. package/dist/x402/facilitator.d.ts.map +1 -1
  87. package/dist/x402/facilitator.js +76 -33
  88. package/dist/x402/facilitator.js.map +1 -1
  89. package/dist/x402/payment-requirements.d.ts +106 -0
  90. package/dist/x402/payment-requirements.d.ts.map +1 -0
  91. package/dist/x402/payment-requirements.js +138 -0
  92. package/dist/x402/payment-requirements.js.map +1 -0
  93. package/dist/x402/relayer.d.ts +41 -0
  94. package/dist/x402/relayer.d.ts.map +1 -0
  95. package/dist/x402/relayer.js +64 -0
  96. package/dist/x402/relayer.js.map +1 -0
  97. package/dist/x402/resource-index.d.ts +75 -0
  98. package/dist/x402/resource-index.d.ts.map +1 -0
  99. package/dist/x402/resource-index.js +126 -0
  100. package/dist/x402/resource-index.js.map +1 -0
  101. package/dist/x402/verified-delivery-facilitator.d.ts +285 -0
  102. package/dist/x402/verified-delivery-facilitator.d.ts.map +1 -0
  103. package/dist/x402/verified-delivery-facilitator.js +423 -0
  104. package/dist/x402/verified-delivery-facilitator.js.map +1 -0
  105. package/dist/zktls/t2-delivery-proof.d.ts +6 -6
  106. package/docs/agent-layer.md +12 -8
  107. package/docs/cli.md +158 -22
  108. package/docs/mcp-hosted.md +4 -3
  109. package/docs/mcp-reference.md +175 -0
  110. package/docs/quickstart.md +18 -4
  111. package/docs/threat-model.md +4 -3
  112. package/docs/x402.md +648 -0
  113. package/package.json +18 -7
  114. package/src/capability/invocation-capability.ts +17 -1
  115. package/src/cli/commands/claim.ts +30 -4
  116. package/src/cli/commands/close.ts +20 -4
  117. package/src/cli/commands/invoke.ts +1 -0
  118. package/src/cli/commands/open.ts +76 -10
  119. package/src/cli/commands/reclaim-unverified.ts +16 -4
  120. package/src/cli/commands/reclaim.ts +22 -5
  121. package/src/cli/commands/streams.ts +1 -1
  122. package/src/cli/deps.ts +115 -24
  123. package/src/cli/privy.ts +396 -0
  124. package/src/cli/spend-policy.ts +139 -0
  125. package/src/cli/wallet.ts +188 -57
  126. package/src/gateway/caller-auth-gateway.ts +77 -6
  127. package/src/gateway/http-server.ts +280 -6
  128. package/src/gateway/x402-mode.ts +350 -0
  129. package/src/index.ts +40 -5
  130. package/src/mandates/env.ts +3 -2
  131. package/src/mcp/server.ts +681 -29
  132. package/src/mcp/service-invoker.ts +285 -0
  133. package/src/sdk/invoke.ts +90 -12
  134. package/src/x402/delivery-oracle.ts +285 -0
  135. package/src/x402/facilitator-server-entry.ts +35 -0
  136. package/src/x402/facilitator-server.ts +445 -0
  137. package/src/x402/facilitator.ts +125 -46
  138. package/src/x402/payment-requirements.ts +231 -0
  139. package/src/x402/relayer.ts +122 -0
  140. package/src/x402/resource-index.ts +228 -0
  141. package/src/x402/verified-delivery-facilitator.ts +744 -0
@@ -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
- export const X402_SCHEME = "streamproof-verified-stream" as const;
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 x402StreamChallengeSchema = z.object({
39
- scheme: z.literal(X402_SCHEME),
63
+ export const streamChallengeSchema = z.object({
64
+ scheme: z.literal(METRIK_STREAM_SCHEME),
40
65
  requirements: verifiedStreamRequirementsSchema,
41
66
  });
42
67
 
43
- export interface X402StreamChallenge extends z.infer<
44
- typeof x402StreamChallengeSchema
68
+ export interface StreamChallenge extends z.infer<
69
+ typeof streamChallengeSchema
45
70
  > {}
46
71
 
47
- export const x402OpenPayloadSchema = z.object({
48
- challenge: x402StreamChallengeSchema,
72
+ export const streamOpenPayloadSchema = z.object({
73
+ challenge: streamChallengeSchema,
49
74
  signedMandate: signedSpendMandateSchema,
50
75
  });
51
76
 
52
- export interface X402OpenPayload extends z.infer<
53
- typeof x402OpenPayloadSchema
77
+ export interface StreamOpenPayload extends z.infer<
78
+ typeof streamOpenPayloadSchema
54
79
  > {}
55
80
 
56
- export interface X402Facilitator {
81
+ export interface StreamFacilitator {
57
82
  /** Build the 402 challenge a server returns to prompt a verified stream. */
58
- challenge(req: VerifiedStreamRequirements): X402StreamChallenge;
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: X402ConsumedHttpsResponseProofInput,
88
+ input: ConsumedHttpsResponseProofInput,
64
89
  ): Promise<VerifiedConsumedHttpsResponseProof>;
65
90
  }
66
91
 
67
- export interface X402ConsumedHttpsResponseProofInput {
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 X402PayloadError extends Error {
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 X402SpendControlUnavailableError extends Error {
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 VerifiedStreamX402Facilitator implements X402Facilitator {
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
- "x402-spent",
185
- "[metrik] x402 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.",
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
- "x402-revocation",
191
- "[metrik] x402 facilitator: no resolveRevokedMandateIds wired — revocation state cannot be verified, so open() will DENY every spend (fail-closed) until a resolver is wired.",
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): X402StreamChallenge {
224
+ challenge(req: VerifiedStreamRequirements): StreamChallenge {
197
225
  const requirements = verifiedStreamRequirementsSchema.parse(req);
198
226
  assertSettlementTarget(requirements, this.sdkConfig);
199
- return { scheme: X402_SCHEME, requirements };
227
+ return { scheme: METRIK_STREAM_SCHEME, requirements };
200
228
  }
201
229
 
202
230
  async open(payloadHeader: string): Promise<{ streamId: `0x${string}` }> {
203
- const payload = parseX402PayloadHeader(payloadHeader);
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 X402SpendControlUnavailableError(
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 X402SpendControlUnavailableError(
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 X402SpendControlUnavailableError(
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 X402SpendControlUnavailableError(
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 X402SpendControlUnavailableError(
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 X402SpendControlUnavailableError(
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: X402ConsumedHttpsResponseProofInput,
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 x402 facilitator",
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 encodeX402PayloadHeader(payload: X402OpenPayload): string {
321
- const parsed = x402OpenPayloadSchema.parse(payload);
322
- const token = Buffer.from(stringifyX402Payload(parsed)).toString("base64url");
323
- return `${X402_SCHEME} ${token}`;
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 parseX402ChallengeJson(value: unknown): X402StreamChallenge {
327
- const parsed = x402StreamChallengeSchema.safeParse(reviveBigInts(value));
356
+ export function parseStreamChallengeJson(value: unknown): StreamChallenge {
357
+ const parsed = streamChallengeSchema.safeParse(reviveBigInts(value));
328
358
  if (!parsed.success) {
329
- throw new X402PayloadError(
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 parseX402PayloadHeader(payloadHeader: string): X402OpenPayload {
366
+ export function parseStreamPayloadHeader(
367
+ payloadHeader: string,
368
+ ): StreamOpenPayload {
337
369
  const [scheme, token] = payloadHeader.split(" ", 2);
338
- if (scheme !== X402_SCHEME || token === undefined || token.trim() === "") {
339
- throw new X402PayloadError("invalid x402 verified-stream payload header");
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 X402PayloadError(
347
- `invalid x402 verified-stream payload encoding: ${
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 = x402OpenPayloadSchema.safeParse(reviveBigInts(decoded));
391
+ const parsed = streamOpenPayloadSchema.safeParse(reviveBigInts(decoded));
354
392
  if (!parsed.success) {
355
- throw new X402PayloadError(
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 stringifyX402Payload(payload: X402OpenPayload): string {
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
+ }