@absol-labs/agent 0.12.0-rc.1 → 0.12.0-rc.2

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 (113) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/claim.d.ts +32 -0
  3. package/dist/cli/commands/claim.d.ts.map +1 -1
  4. package/dist/cli/commands/claim.js +115 -27
  5. package/dist/cli/commands/claim.js.map +1 -1
  6. package/dist/cli/commands/pay.d.ts +15 -0
  7. package/dist/cli/commands/pay.d.ts.map +1 -1
  8. package/dist/cli/commands/pay.js +131 -7
  9. package/dist/cli/commands/pay.js.map +1 -1
  10. package/dist/cli/commands/reclaim.d.ts.map +1 -1
  11. package/dist/cli/commands/reclaim.js +20 -4
  12. package/dist/cli/commands/reclaim.js.map +1 -1
  13. package/dist/cli/commands/refund.d.ts +19 -1
  14. package/dist/cli/commands/refund.d.ts.map +1 -1
  15. package/dist/cli/commands/refund.js +200 -27
  16. package/dist/cli/commands/refund.js.map +1 -1
  17. package/dist/cli/commands/status.d.ts.map +1 -1
  18. package/dist/cli/commands/status.js +11 -1
  19. package/dist/cli/commands/status.js.map +1 -1
  20. package/dist/cli/commands/watch.d.ts.map +1 -1
  21. package/dist/cli/commands/watch.js +2 -0
  22. package/dist/cli/commands/watch.js.map +1 -1
  23. package/dist/cli/context.d.ts +19 -0
  24. package/dist/cli/context.d.ts.map +1 -1
  25. package/dist/cli/deps.d.ts +20 -0
  26. package/dist/cli/deps.d.ts.map +1 -1
  27. package/dist/cli/deps.js +14 -0
  28. package/dist/cli/deps.js.map +1 -1
  29. package/dist/cli/progress.d.ts +13 -1
  30. package/dist/cli/progress.d.ts.map +1 -1
  31. package/dist/cli/progress.js +7 -1
  32. package/dist/cli/progress.js.map +1 -1
  33. package/dist/cli/run.d.ts.map +1 -1
  34. package/dist/cli/run.js +26 -14
  35. package/dist/cli/run.js.map +1 -1
  36. package/dist/cli/status.d.ts +103 -0
  37. package/dist/cli/status.d.ts.map +1 -1
  38. package/dist/cli/status.js +310 -41
  39. package/dist/cli/status.js.map +1 -1
  40. package/dist/cli/wallet.d.ts +1 -1
  41. package/dist/cli/wallet.d.ts.map +1 -1
  42. package/dist/cli/wallet.js +28 -4
  43. package/dist/cli/wallet.js.map +1 -1
  44. package/dist/gateway/http-server.d.ts +17 -0
  45. package/dist/gateway/http-server.d.ts.map +1 -1
  46. package/dist/gateway/http-server.js +76 -9
  47. package/dist/gateway/http-server.js.map +1 -1
  48. package/dist/gateway/x402-opener-mode.d.ts +39 -2
  49. package/dist/gateway/x402-opener-mode.d.ts.map +1 -1
  50. package/dist/gateway/x402-opener-mode.js +169 -36
  51. package/dist/gateway/x402-opener-mode.js.map +1 -1
  52. package/dist/mcp/server.d.ts +12 -0
  53. package/dist/mcp/server.d.ts.map +1 -1
  54. package/dist/mcp/server.js +28 -9
  55. package/dist/mcp/server.js.map +1 -1
  56. package/dist/mcp/x402-tools.d.ts +8 -3
  57. package/dist/mcp/x402-tools.d.ts.map +1 -1
  58. package/dist/mcp/x402-tools.js +126 -6
  59. package/dist/mcp/x402-tools.js.map +1 -1
  60. package/dist/sdk/client.d.ts +13 -0
  61. package/dist/sdk/client.d.ts.map +1 -1
  62. package/dist/sdk/client.js +15 -0
  63. package/dist/sdk/client.js.map +1 -1
  64. package/dist/x402-buyer/pay.d.ts +22 -0
  65. package/dist/x402-buyer/pay.d.ts.map +1 -1
  66. package/dist/x402-buyer/pay.js +33 -3
  67. package/dist/x402-buyer/pay.js.map +1 -1
  68. package/dist/x402-opener/facilitator-entry.js +4 -1
  69. package/dist/x402-opener/facilitator-entry.js.map +1 -1
  70. package/dist/x402-opener/facilitator-server.d.ts +29 -0
  71. package/dist/x402-opener/facilitator-server.d.ts.map +1 -1
  72. package/dist/x402-opener/facilitator-server.js +67 -1
  73. package/dist/x402-opener/facilitator-server.js.map +1 -1
  74. package/dist/x402-opener/refund-relay.d.ts +30 -4
  75. package/dist/x402-opener/refund-relay.d.ts.map +1 -1
  76. package/dist/x402-opener/refund-relay.js +123 -22
  77. package/dist/x402-opener/refund-relay.js.map +1 -1
  78. package/dist/x402-opener/scheme.d.ts +79 -0
  79. package/dist/x402-opener/scheme.d.ts.map +1 -1
  80. package/dist/x402-opener/scheme.js +326 -40
  81. package/dist/x402-opener/scheme.js.map +1 -1
  82. package/dist/x402-opener/terms.d.ts +61 -1
  83. package/dist/x402-opener/terms.d.ts.map +1 -1
  84. package/dist/x402-opener/terms.js +94 -2
  85. package/dist/x402-opener/terms.js.map +1 -1
  86. package/docs/cli.md +87 -9
  87. package/docs/mcp-reference.md +18 -2
  88. package/docs/quickstart.md +3 -3
  89. package/docs/x402.md +150 -10
  90. package/package.json +3 -3
  91. package/src/cli/commands/claim.ts +167 -32
  92. package/src/cli/commands/pay.ts +150 -8
  93. package/src/cli/commands/reclaim.ts +25 -5
  94. package/src/cli/commands/refund.ts +231 -33
  95. package/src/cli/commands/status.ts +11 -1
  96. package/src/cli/commands/watch.ts +4 -0
  97. package/src/cli/context.ts +16 -0
  98. package/src/cli/deps.ts +37 -0
  99. package/src/cli/progress.ts +21 -2
  100. package/src/cli/run.ts +26 -13
  101. package/src/cli/status.ts +440 -43
  102. package/src/cli/wallet.ts +44 -7
  103. package/src/gateway/http-server.ts +96 -8
  104. package/src/gateway/x402-opener-mode.ts +234 -37
  105. package/src/mcp/server.ts +43 -11
  106. package/src/mcp/x402-tools.ts +171 -7
  107. package/src/sdk/client.ts +25 -0
  108. package/src/x402-buyer/pay.ts +41 -3
  109. package/src/x402-opener/facilitator-entry.ts +4 -1
  110. package/src/x402-opener/facilitator-server.ts +101 -1
  111. package/src/x402-opener/refund-relay.ts +150 -20
  112. package/src/x402-opener/scheme.ts +459 -41
  113. package/src/x402-opener/terms.ts +155 -2
@@ -1,4 +1,8 @@
1
- import { streamEscrowV2Abi } from "@absol-labs/shared";
1
+ import {
2
+ streamEscrowV2Abi,
3
+ x402UpfrontStreamOpenerAbi,
4
+ x402UpfrontStreamOpenerFactoryAbi,
5
+ } from "@absol-labs/shared";
2
6
  import {
3
7
  BaseError,
4
8
  ContractFunctionRevertedError,
@@ -23,7 +27,7 @@ import {
23
27
 
24
28
  import { x402StreamOpenerAbi, x402StreamOpenerFactoryAbi } from "./abi.js";
25
29
  import { RelayerQueue } from "./relayer-queue.js";
26
- import { chainIdFromNetwork } from "./terms.js";
30
+ import { chainIdFromNetwork, type OpenerKind } from "./terms.js";
27
31
  import {
28
32
  escrowGenerationFor,
29
33
  readEscapeHatchReadyAt,
@@ -60,9 +64,18 @@ import {
60
64
  *
61
65
  * ## Bounded against gas abuse
62
66
  *
63
- * - Only streams whose escrow buyer is an opener of the configured factory
64
- * (`factory.isOpener(stream.buyer)`, on-chain) are served; everything else is
65
- * `404` before any signature check, oracle call or simulation.
67
+ * - Only streams whose escrow buyer is an opener of a configured factory
68
+ * (`factory.isOpener(stream.buyer)`, on-chain: the stream factory, and the
69
+ * upfront factory when configured) are served; everything else is `404`
70
+ * before any signature check, oracle call or simulation. Which factory
71
+ * claims the opener decides its KIND, and so the EIP-712 domain its payer
72
+ * signs under ("Metrik X402StreamOpener" or "Metrik X402UpfrontStreamOpener").
73
+ * - Stream id 0 is an UPFRONT-ONLY payment (`upfrontAmount == price`): no
74
+ * stream exists and nothing was escrowed. Every route answers
75
+ * `nothing-to-refund` for it without a chain read, and no close or reclaim is
76
+ * ever submitted for it.
77
+ * - For an upfront opener, only the stream deposit (`price - upfrontAmount`)
78
+ * is refundable; the status reports that as `maxRefund`, never the price.
66
79
  * - Every precondition the contract would check is read first (status, expiry,
67
80
  * settlement window, reclaimed flag, non-zero refund). A request that would
68
81
  * have no real effect never reaches the relayer.
@@ -84,6 +97,18 @@ import {
84
97
  /** EIP-712 domain name/version of `X402StreamOpener`. Checked against the opener's ERC-5267 domain. */
85
98
  export const OPENER_EIP712_NAME = "Metrik X402StreamOpener";
86
99
  export const OPENER_EIP712_VERSION = "1";
100
+ /** EIP-712 domain name of `X402UpfrontStreamOpener` (same version, same types). */
101
+ export const UPFRONT_OPENER_EIP712_NAME = "Metrik X402UpfrontStreamOpener";
102
+
103
+ /** The EIP-712 domain name an opener of `kind` signs under. */
104
+ export function openerEip712Name(kind: OpenerKind): string {
105
+ return kind === "upfront" ? UPFRONT_OPENER_EIP712_NAME : OPENER_EIP712_NAME;
106
+ }
107
+
108
+ /** True for the all-zero bytes32: an upfront-only payment, no stream. */
109
+ export function isNoStreamId(streamId: string): boolean {
110
+ return /^0x0{64}$/i.test(streamId);
111
+ }
87
112
 
88
113
  /** The two payer-signed opener actions. */
89
114
  export type OpenerSignedAction = "Close" | "ReclaimUnverified";
@@ -118,6 +143,8 @@ const CHECKPOINT_RETRY_HINT_SECONDS = 30;
118
143
  */
119
144
  export function openerActionTypedData(input: {
120
145
  readonly action: OpenerSignedAction;
146
+ /** The opener's kind (read on-chain); default `"stream"`. Picks the domain name. */
147
+ readonly kind?: OpenerKind;
121
148
  readonly opener: Address;
122
149
  readonly chainId: number;
123
150
  readonly streamId: Hex;
@@ -126,7 +153,7 @@ export function openerActionTypedData(input: {
126
153
  }) {
127
154
  return {
128
155
  domain: {
129
- name: OPENER_EIP712_NAME,
156
+ name: openerEip712Name(input.kind ?? "stream"),
130
157
  version: OPENER_EIP712_VERSION,
131
158
  chainId: input.chainId,
132
159
  verifyingContract: getAddress(input.opener),
@@ -178,6 +205,11 @@ export interface OpenerRefundRelayConfig {
178
205
  /** CAIP-2 network (`eip155:<chainId>`), as the registry gives it. Must match the RPC's chain id. */
179
206
  readonly network: string;
180
207
  readonly factoryAddress: Address;
208
+ /**
209
+ * The `X402UpfrontStreamOpenerFactory` whose clones are served too (same
210
+ * escrow, checked by {@link OpenerRefundRelay.init}). Unset = stream openers only.
211
+ */
212
+ readonly upfrontFactoryAddress?: Address;
181
213
  /** The `StreamEscrowV2` the factory settles into (checked by {@link OpenerRefundRelay.init}). */
182
214
  readonly escrowAddress: Address;
183
215
  /** Base URL of the Metrik oracle that serves `GET /checkpoint/proof?streamId=`. */
@@ -290,6 +322,9 @@ interface ResolvedStream {
290
322
  readonly stream: OpenerStream;
291
323
  readonly opener: Address;
292
324
  readonly payer: Address;
325
+ /** Which configured factory's `isOpener` claims `opener` (read on-chain). */
326
+ readonly kind: OpenerKind;
327
+ readonly factory: Address;
293
328
  }
294
329
 
295
330
  type OracleProof =
@@ -379,6 +414,7 @@ class MinuteWindows {
379
414
  export class OpenerRefundRelay implements RefundRelayHandler {
380
415
  private readonly chainId: number;
381
416
  private readonly factory: Address;
417
+ private readonly upfrontFactory: Address | undefined;
382
418
  private readonly escrow: Address;
383
419
  private readonly oracleUrl: string;
384
420
  private readonly queue: RelayerQueue;
@@ -409,6 +445,19 @@ export class OpenerRefundRelay implements RefundRelayHandler {
409
445
  throw new Error("factoryAddress and escrowAddress must be EVM addresses");
410
446
  }
411
447
  this.factory = getAddress(config.factoryAddress);
448
+ if (
449
+ config.upfrontFactoryAddress !== undefined &&
450
+ !isAddress(config.upfrontFactoryAddress)
451
+ ) {
452
+ throw new Error("upfrontFactoryAddress must be an EVM address");
453
+ }
454
+ this.upfrontFactory =
455
+ config.upfrontFactoryAddress === undefined
456
+ ? undefined
457
+ : getAddress(config.upfrontFactoryAddress);
458
+ if (this.upfrontFactory === this.factory) {
459
+ throw new Error("upfrontFactoryAddress must differ from factoryAddress");
460
+ }
412
461
  this.escrow = getAddress(config.escrowAddress);
413
462
  const generation =
414
463
  config.escrowGeneration ?? escrowGenerationFor(this.chainId, this.escrow);
@@ -487,6 +536,18 @@ export class OpenerRefundRelay implements RefundRelayHandler {
487
536
  `refund relay: factory escrow ${factoryEscrow} is not the configured escrow ${this.escrow}`,
488
537
  );
489
538
  }
539
+ if (this.upfrontFactory !== undefined) {
540
+ const upfrontEscrow = await publicClient.readContract({
541
+ address: this.upfrontFactory,
542
+ abi: x402UpfrontStreamOpenerFactoryAbi,
543
+ functionName: "escrow",
544
+ });
545
+ if (getAddress(upfrontEscrow) !== this.escrow) {
546
+ throw new Error(
547
+ `refund relay: upfront factory escrow ${upfrontEscrow} is not the configured escrow ${this.escrow}`,
548
+ );
549
+ }
550
+ }
490
551
  this.escapeGraceSeconds = BigInt(
491
552
  await publicClient.readContract({
492
553
  address: this.escrow,
@@ -501,6 +562,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
501
562
  async status(rawStreamId: string): Promise<RefundRelayResponse> {
502
563
  const streamId = parseStreamId(rawStreamId);
503
564
  if (streamId === undefined) return invalidStreamId();
565
+ if (isNoStreamId(streamId)) return nothingToRefund(404);
504
566
  const cached = this.fresh(this.statusCache, streamId);
505
567
  if (cached !== undefined) return cached.response;
506
568
  const response = await this.computeStatus(streamId);
@@ -517,12 +579,15 @@ export class OpenerRefundRelay implements RefundRelayHandler {
517
579
  try {
518
580
  const resolved = await this.resolve(rawStreamId);
519
581
  if ("status" in resolved) return resolved;
520
- const { streamId, stream, opener, payer } = resolved;
582
+ const { streamId, stream, opener, payer, kind, factory } = resolved;
521
583
  const grace = this.grace();
522
- const [now, nonce, latestCheckedAt] = await Promise.all([
584
+ const [now, nonce, latestCheckedAt, upfrontTerms] = await Promise.all([
523
585
  this.chainNow(),
524
586
  this.readNonce(opener, payer),
525
587
  this.readLatestCheckedAt(),
588
+ kind === "upfront"
589
+ ? this.readUpfrontTerms(opener)
590
+ : Promise.resolve(undefined),
526
591
  ]);
527
592
  const closed = stream.status === 1;
528
593
  const stopAt = stopTime(stream);
@@ -571,6 +636,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
571
636
  typedData: jsonTypedData(
572
637
  openerActionTypedData({
573
638
  action: "Close",
639
+ kind,
574
640
  opener,
575
641
  chainId: this.chainId,
576
642
  streamId,
@@ -617,6 +683,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
617
683
  typedData: jsonTypedData(
618
684
  openerActionTypedData({
619
685
  action: "ReclaimUnverified",
686
+ kind,
620
687
  opener,
621
688
  chainId: this.chainId,
622
689
  streamId,
@@ -643,12 +710,24 @@ export class OpenerRefundRelay implements RefundRelayHandler {
643
710
  streamId,
644
711
  chainId: this.chainId,
645
712
  escrow: this.escrow,
646
- factory: this.factory,
713
+ factory,
714
+ openerKind: kind,
647
715
  opener,
648
716
  payer,
649
717
  operator: stream.operator,
650
718
  serviceRef: stream.serviceRef,
651
719
  deposit: stream.deposit.toString(),
720
+ // The most any refund of this payment can return: the escrowed
721
+ // deposit. For an upfront opener that is price - upfrontAmount; the
722
+ // upfront part was paid to the seller at settlement and is final.
723
+ maxRefund: stream.deposit.toString(),
724
+ ...(upfrontTerms === undefined
725
+ ? {}
726
+ : {
727
+ price: upfrontTerms.price.toString(),
728
+ upfrontAmount: upfrontTerms.upfrontAmount.toString(),
729
+ upfrontNote: `${upfrontTerms.upfrontAmount.toString()} of the ${upfrontTerms.price.toString()} paid was a non-refundable per-call charge, paid to the seller at settlement; at most the ${stream.deposit.toString()} escrowed can be refunded.`,
730
+ }),
652
731
  ratePerSecond: stream.ratePerSecond.toString(),
653
732
  claimedCumulative: stream.claimedCumulative.toString(),
654
733
  openedAt: Number(stream.openedAt),
@@ -687,6 +766,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
687
766
  ): Promise<RefundRelayResponse> {
688
767
  const streamId = parseStreamId(rawStreamId);
689
768
  if (streamId === undefined) return invalidStreamId();
769
+ if (isNoStreamId(streamId)) return nothingToRefund(409);
690
770
  const signed = parseSignedBody(body);
691
771
  if ("status" in signed) return signed;
692
772
  const key = ledgerKey("close", streamId);
@@ -695,7 +775,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
695
775
  try {
696
776
  const resolved = await this.resolve(streamId);
697
777
  if ("status" in resolved) return resolved;
698
- const { stream, opener, payer } = resolved;
778
+ const { stream, opener, payer, kind } = resolved;
699
779
  const now = await this.chainNow();
700
780
  if (stream.status === 1) {
701
781
  return fail(409, "already-closed", "the stream is already closed", {
@@ -712,6 +792,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
712
792
  }
713
793
  const refused = await this.verifyPayerSignature(
714
794
  "Close",
795
+ kind,
715
796
  streamId,
716
797
  opener,
717
798
  payer,
@@ -740,6 +821,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
740
821
  async reclaim(rawStreamId: string): Promise<RefundRelayResponse> {
741
822
  const streamId = parseStreamId(rawStreamId);
742
823
  if (streamId === undefined) return invalidStreamId();
824
+ if (isNoStreamId(streamId)) return nothingToRefund(409);
743
825
  const key = ledgerKey("reclaim", streamId);
744
826
  const early = await this.precheck(key, "reclaim");
745
827
  if (early !== undefined) return early;
@@ -832,6 +914,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
832
914
  ): Promise<RefundRelayResponse> {
833
915
  const streamId = parseStreamId(rawStreamId);
834
916
  if (streamId === undefined) return invalidStreamId();
917
+ if (isNoStreamId(streamId)) return nothingToRefund(409);
835
918
  const signed = parseSignedBody(body);
836
919
  if ("status" in signed) return signed;
837
920
  const key = ledgerKey("reclaim", streamId);
@@ -840,7 +923,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
840
923
  try {
841
924
  const resolved = await this.resolve(streamId);
842
925
  if ("status" in resolved) return resolved;
843
- const { stream, opener, payer } = resolved;
926
+ const { stream, opener, payer, kind } = resolved;
844
927
  if (stream.reclaimed) return alreadyReclaimed();
845
928
  const now = await this.chainNow();
846
929
  const escapeAt = await this.escapeHatchAt(streamId, stream);
@@ -868,6 +951,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
868
951
  }
869
952
  const refused = await this.verifyPayerSignature(
870
953
  "ReclaimUnverified",
954
+ kind,
871
955
  streamId,
872
956
  opener,
873
957
  payer,
@@ -1236,13 +1320,31 @@ export class OpenerRefundRelay implements RefundRelayHandler {
1236
1320
  }
1237
1321
  if (!isAddress(stream.buyer)) return notFound();
1238
1322
  const buyer = getAddress(stream.buyer);
1239
- const isOpener = await publicClient.readContract({
1240
- address: this.factory,
1241
- abi: x402StreamOpenerFactoryAbi,
1242
- functionName: "isOpener",
1243
- args: [buyer],
1244
- });
1245
- if (isOpener !== true) return notFound();
1323
+ // The kind is whichever CONFIGURED factory's isOpener claims the buyer,
1324
+ // read on-chain — never inferred from the address, code or the request.
1325
+ const [isStreamOpener, isUpfrontOpener] = await Promise.all([
1326
+ publicClient.readContract({
1327
+ address: this.factory,
1328
+ abi: x402StreamOpenerFactoryAbi,
1329
+ functionName: "isOpener",
1330
+ args: [buyer],
1331
+ }),
1332
+ this.upfrontFactory === undefined
1333
+ ? Promise.resolve(false)
1334
+ : publicClient.readContract({
1335
+ address: this.upfrontFactory,
1336
+ abi: x402UpfrontStreamOpenerFactoryAbi,
1337
+ functionName: "isOpener",
1338
+ args: [buyer],
1339
+ }),
1340
+ ]);
1341
+ if ((isStreamOpener === true) === (isUpfrontOpener === true)) {
1342
+ // Neither (not our opener) — or both, which CREATE2 makes impossible.
1343
+ return notFound();
1344
+ }
1345
+ const kind: OpenerKind = isUpfrontOpener === true ? "upfront" : "stream";
1346
+ const factory =
1347
+ kind === "upfront" ? (this.upfrontFactory as Address) : this.factory;
1246
1348
  const payer = await publicClient.readContract({
1247
1349
  address: buyer,
1248
1350
  abi: x402StreamOpenerAbi,
@@ -1254,6 +1356,8 @@ export class OpenerRefundRelay implements RefundRelayHandler {
1254
1356
  streamId,
1255
1357
  opener: buyer,
1256
1358
  payer: getAddress(payer),
1359
+ kind,
1360
+ factory,
1257
1361
  stream: {
1258
1362
  buyer,
1259
1363
  operator: getAddress(stream.operator),
@@ -1274,6 +1378,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
1274
1378
  /** Off-chain check of a payer-signed opener action. `undefined` = valid. */
1275
1379
  private async verifyPayerSignature(
1276
1380
  action: OpenerSignedAction,
1381
+ kind: OpenerKind,
1277
1382
  streamId: Hex,
1278
1383
  opener: Address,
1279
1384
  payer: Address,
@@ -1322,7 +1427,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
1322
1427
  readonly bigint[],
1323
1428
  ];
1324
1429
  if (
1325
- name !== OPENER_EIP712_NAME ||
1430
+ name !== openerEip712Name(kind) ||
1326
1431
  version !== OPENER_EIP712_VERSION ||
1327
1432
  BigInt(chainId) !== BigInt(this.chainId) ||
1328
1433
  getAddress(verifyingContract) !== opener
@@ -1340,6 +1445,7 @@ export class OpenerRefundRelay implements RefundRelayHandler {
1340
1445
  address: payer,
1341
1446
  ...openerActionTypedData({
1342
1447
  action,
1448
+ kind,
1343
1449
  opener,
1344
1450
  chainId: this.chainId,
1345
1451
  streamId,
@@ -1380,6 +1486,21 @@ export class OpenerRefundRelay implements RefundRelayHandler {
1380
1486
  );
1381
1487
  }
1382
1488
 
1489
+ /** An upfront opener's immutable terms (`price`, `upfrontAmount`). */
1490
+ private async readUpfrontTerms(
1491
+ opener: Address,
1492
+ ): Promise<{ readonly price: bigint; readonly upfrontAmount: bigint }> {
1493
+ const terms = (await this.config.publicClient.readContract({
1494
+ address: opener,
1495
+ abi: x402UpfrontStreamOpenerAbi,
1496
+ functionName: "terms",
1497
+ })) as unknown as { price: bigint; upfrontAmount: bigint };
1498
+ return {
1499
+ price: BigInt(terms.price),
1500
+ upfrontAmount: BigInt(terms.upfrontAmount),
1501
+ };
1502
+ }
1503
+
1383
1504
  private async readLatestCheckedAt(): Promise<bigint> {
1384
1505
  return BigInt(
1385
1506
  await this.config.publicClient.readContract({
@@ -1672,7 +1793,16 @@ function notFound(): RefundRelayResponse {
1672
1793
  return fail(
1673
1794
  404,
1674
1795
  "not-found",
1675
- "no stream opened through this facilitator's X402StreamOpenerFactory has that id",
1796
+ "no stream opened through this facilitator's opener factories has that id",
1797
+ );
1798
+ }
1799
+
1800
+ /** Stream id 0: an upfront-only payment. Never read, closed or reclaimed. */
1801
+ function nothingToRefund(status: 404 | 409): RefundRelayResponse {
1802
+ return fail(
1803
+ status,
1804
+ "nothing-to-refund",
1805
+ "stream id 0 is an upfront-only payment (upfrontAmount == price): the whole price was paid to the seller at settlement, no stream was opened, nothing was escrowed, and there is nothing to close or reclaim",
1676
1806
  );
1677
1807
  }
1678
1808