@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
@@ -111,7 +111,7 @@ const account = wallet.account;
111
111
  const mandate = spendMandateSchema.parse({
112
112
  maxPerStreamUsdc: 100_000n, // 0.1 USDC (6 decimals) — max deposit per stream
113
113
  maxTotalUsdc: 100_000n, // cumulative cap across all streams
114
- maxRatePerSecondUsdc: 1n, // max accrual rate the agent may agree to
114
+ maxRatePerSecondUsdc: 100n, // max accrual rate the agent may agree to (0.0001 USDC/s)
115
115
  maxDurationSeconds: 3600, // max stream lifetime
116
116
  allowedOperators: [account.address as `0x${string}`], // omit/empty ⇒ any operator
117
117
  expiresAt: nowSeconds + 365 * 24 * 60 * 60,
@@ -228,7 +228,7 @@ const hire = await agent.openVerifiedStream({
228
228
  operator: target.operator, // seller payout wallet from discovery
229
229
  serviceRef: target.serviceRef, // bytes32 the oracle verifies delivery against
230
230
  budgetUsdc: 100_000n,
231
- ratePerSecondUsdc: 1n,
231
+ ratePerSecondUsdc: 100n, // must sit inside the listing's signed rate band
232
232
  maxDurationSeconds: 3600,
233
233
  signedMandate,
234
234
  spentSoFarUsdc: 0n,
@@ -340,7 +340,7 @@ loop (`discover -> open -> invoke -> watch -> close -> reclaim`):
340
340
  ```bash
341
341
  npx --package @absol-labs/agent metrik discover --min-tier 2
342
342
  npx --package @absol-labs/agent metrik open --service <serviceRef> \
343
- --rate 0.000001 --budget 0.27 --duration 1h --dry-run
343
+ --rate 0.0001 --budget 0.36 --duration 1h --dry-run
344
344
  ```
345
345
 
346
346
  Testnet-only, key read from `METRIK_AGENT_PRIVATE_KEY`, `--json` for scripting. Full
package/docs/x402.md CHANGED
@@ -10,6 +10,16 @@
10
10
  > `0xbD8B7B2F49Fdb954bda6bE5ce22218d44345DC43`, block 47337415); everything
11
11
  > here is testnet only.
12
12
  >
13
+ > **Upfront ("minimum charge") opener.** A listing can instead quote the
14
+ > `X402UpfrontStreamOpenerFactory` (metrik-contracts#54; Base Sepolia
15
+ > `0x17126c80C12a51183d3664EEE2b602552592c2Aa`, implementation
16
+ > `0xA9C58A2554Eb869B0D1ECBa46a0435cFFC90341d`, bound to StreamEscrowV3
17
+ > `0x0643ba482C6A7aF31a1f8225e7e7e79Cfc467b13`): part of each payment is a
18
+ > NON-REFUNDABLE per-call charge, only the rest is escrowed and metered. See
19
+ > [The upfront (minimum-charge) opener](#the-upfront-minimum-charge-opener).
20
+ > The V3-bound stream factory `0x3d3a…cFB2` stays live, and its clones are
21
+ > still served and refunded.
22
+ >
13
23
  > The separate Metrik-native `402` flow (scheme `streamproof-verified-stream`,
14
24
  > header `X-METRIK-PAYMENT`) described at the end is **not** x402 and is
15
25
  > unchanged.
@@ -33,9 +43,11 @@ So, in this package, and for anyone integrating it:
33
43
  payment into the opener and opens the stream in the same transaction. Its
34
44
  `GET /supported` advertises a marker —
35
45
  `kinds[].extra.metrikStreamOpener = {settlement: "x402-stream-opener",
36
- factory, escrow, usdc, chainId, minAmount, receiptTimeoutSeconds, refundRelay?}` — so a resource server can tell it apart
46
+ factory, escrow, usdc, chainId, minAmount, receiptTimeoutSeconds, refundRelay?, upfrontOpener?}` — so a resource server can tell it apart
37
47
  from a stock facilitator. `refundRelay: true` means the same service also
38
- runs the gasless refund relay (see below).
48
+ runs the gasless refund relay (see below). `upfrontOpener = {factory,
49
+ implementation, escrow, termsVersion: "upfront-1"}` is present when it also
50
+ settles the upfront opener kind; `factory` stays the stream factory.
39
51
  2. **The gateway refuses to start** unless its configured facilitator's
40
52
  `/supported` carries that marker for exactly the gateway's factory, escrow,
41
53
  token and chain. It re-checks the marker on a timer (default every 300 s)
@@ -146,6 +158,93 @@ deployment would need a shared store, which does not exist today. Entries are
146
158
  kept until the authorization's `validBefore` has passed (plus a grace), after
147
159
  which USDC itself refuses the authorization.
148
160
 
161
+ ## The upfront (minimum-charge) opener
162
+
163
+ A stream-opener payment funds a stream of the WHOLE payment, and accrual starts
164
+ only at the first verified interval: a buyer who takes the response and closes
165
+ within seconds reclaims almost all of it, so one delivered call can cost
166
+ nothing. The upfront opener (metrik-contracts#54, design note
167
+ `docs/x402-opener-min-charge.md`) fixes that with a new opener and factory;
168
+ nothing deployed changed.
169
+
170
+ - **Terms** gain two fields: `{operator, serviceRef, ratePerSecond,
171
+ maxDuration, price, upfrontAmount}`. All six are committed by `payTo =
172
+ upfrontFactory.predict(terms)` (CREATE2 over `abi.encode(terms)`), so the
173
+ payer's EIP-3009 signature binds the split too.
174
+ - **`amount == price`**, exactly: the opener rejects any other value on-chain
175
+ (`PriceMismatch`) before anything moves.
176
+ - **At settlement, in the same transaction**, `upfrontAmount` is paid to the
177
+ seller (minus the escrow's protocol fee, rounded up, to its fee recipient)
178
+ and is **NOT refundable**: it pays for the call whatever is delivered. Only
179
+ `price - upfrontAmount` funds a `StreamEscrowV3` stream, metered exactly like
180
+ a stream opener's, and that deposit is the most any refund can return.
181
+ - **`upfrontAmount == price` is upfront-only**: no stream is opened at all. The
182
+ settlement reports `streamId: null` (on-chain `bytes32(0)`), the response is
183
+ served, no metered session exists, and there is nothing to close, reclaim or
184
+ refund.
185
+
186
+ ### The 402 `extra` (what the SDK enforces before signing)
187
+
188
+ ```json
189
+ {
190
+ "factory": "<registry upfront factory>",
191
+ "termsVersion": "upfront-1",
192
+ "operator": "0x…",
193
+ "serviceRef": "0x…",
194
+ "ratePerSecond": "100",
195
+ "maxDuration": "3600",
196
+ "price": "60000",
197
+ "upfrontAmount": "15000",
198
+ "refundUrl": "https://<relay>/streams/{streamId}"
199
+ }
200
+ ```
201
+
202
+ under `extra.metrikStreamOpener`, with `amount == price` and `payTo ==
203
+ predict(6 terms)`. An upfront-only quote carries no `refundUrl`. The SDK buyer
204
+ (`payWithX402`, 0.10.0-rc.3) recomputes `payTo` OFFLINE from the registry's
205
+ factory and implementation, runs `validateUpfrontTerms`, requires `price -
206
+ upfrontAmount <= ratePerSecond × maxDuration`, and refuses an upfront part above
207
+ `maxUpfrontAmount` (default 0.10 USDC; `0` refuses any).
208
+
209
+ The settlement's `PAYMENT-RESPONSE.extra.metrikStreamOpener` is `{streamId
210
+ (null when upfront-only), opener, escrow, deposit (= price - upfrontAmount),
211
+ ratePerSecond, expiresAt (only with a stream), upfrontAmount, upfrontFee,
212
+ refundUrl (only with a stream)}`; `upfrontAmount` and `upfrontFee` are read from
213
+ the opener's `UpfrontCharged` event in the mined receipt.
214
+
215
+ ### What the facilitator checks for an upfront payment
216
+
217
+ Everything in the table above, plus: the factory is the upfront factory this
218
+ facilitator is bound to (anything not one of its configured factories is
219
+ `metrik_opener_factory_mismatch`); the six terms pass the factory's own
220
+ `validateTerms` rules and `price - upfrontAmount <= ratePerSecond ×
221
+ maxDuration` (`metrik_opener_invalid_terms`); a `termsVersion`, when present,
222
+ is `upfront-1`; `amount == price` (`metrik_opener_invalid_amount`); `payTo ==
223
+ upfrontFactory.predict(terms)` on-chain; and an `eth_call` of the 6-tuple
224
+ `deployAndSettle`. Price/upfront fields on a STREAM-factory quote are refused
225
+ (that `payTo` does not commit to them). `settle` reports success only when the
226
+ receipt shows exactly the signed split: `UpfrontCharged` for this payer, nonce,
227
+ operator and `upfrontAmount` (when non-zero), and — unless upfront-only — the
228
+ escrow's `StreamOpened` and the opener's `StreamOpenedFor` for a deposit of
229
+ exactly `price - upfrontAmount`; an upfront-only receipt must show
230
+ `UpfrontCharged` with `streamId 0` and no stream.
231
+
232
+ ### Running an upfront listing
233
+
234
+ On the gateway, set `METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT` (base units) next to
235
+ the usual `METRIK_GATEWAY_X402V2_*` variables; its presence selects the upfront
236
+ kind, and the factory defaults to the registry's upfront factory
237
+ (`METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS` overrides it;
238
+ `METRIK_GATEWAY_X402V2_FACTORY_ADDRESS`, which names the stream factory, is
239
+ refused alongside it). At startup the gateway validates the six terms, reads
240
+ the factory's `escrow()`, `usdc()` and `implementation()`, and requires its
241
+ on-chain `predict` to equal the offline CREATE2 derivation the SDK uses; the
242
+ facilitator's marker must advertise exactly this upfront factory in
243
+ `upfrontOpener`. Size `upfrontAmount` to at least the marginal cost of serving
244
+ one call; use `upfrontAmount == price` for calls with nothing ongoing to meter.
245
+ The facilitator settles the registry's upfront factory by default (see
246
+ `METRIK_OPENER_FACILITATOR_UPFRONT_FACTORY_ADDRESS`).
247
+
149
248
  ## Getting unearned money back: the gasless refund relay
150
249
 
151
250
  **There is no automatic refund.** Nothing pushes money back to a payer, and no
@@ -177,6 +276,16 @@ The streamId and the refund URL arrive with the paid response:
177
276
  `PAYMENT-RESPONSE.extra.metrikStreamOpener = {streamId, opener, escrow,
178
277
  deposit, ratePerSecond, expiresAt, refundUrl}`.
179
278
 
279
+ **Upfront openers.** The relay serves the upfront factory's clones too. Which
280
+ factory's `isOpener` claims the stream's buyer, read on-chain, decides the
281
+ opener's kind and so the EIP-712 domain the payer signs under (`"Metrik
282
+ X402UpfrontStreamOpener"` instead of `"Metrik X402StreamOpener"`); the status
283
+ reports `openerKind`, `price`, `upfrontAmount` and `maxRefund` (the deposit, =
284
+ `price - upfrontAmount`) — the upfront part was paid at settlement and can
285
+ never come back. **Stream id 0** is an upfront-only payment: every route
286
+ answers `nothing-to-refund` (`404` for the status, `409` for the actions)
287
+ without a chain read, and no close or reclaim is ever submitted for it.
288
+
180
289
  ### Endpoints (on the facilitator service; public, no bearer token)
181
290
 
182
291
  | Method + path | Body | Does |
@@ -338,6 +447,7 @@ npx metrik-x402-opener-facilitator
338
447
  | `METRIK_OPENER_FACILITATOR_RELAYER_PRIVATE_KEY` | required | Pays gas for `deployAndSettle`. Never holds buyer funds; never logged. |
339
448
  | `METRIK_OPENER_FACILITATOR_AUTH_TOKEN` | required (≥ 32 chars) | Bearer token required on `/settle`. |
340
449
  | `METRIK_OPENER_FACILITATOR_FACTORY_ADDRESS` | the Base Sepolia factory | Required on any other chain. |
450
+ | `METRIK_OPENER_FACILITATOR_UPFRONT_FACTORY_ADDRESS` | the registry's, for the escrow | Upfront opener factory settled too (same escrow and token); `off` disables it. |
341
451
  | `METRIK_OPENER_FACILITATOR_ESCROW_ADDRESS` | live Base Sepolia `StreamEscrowV2` | Must equal `factory.escrow()`. |
342
452
  | `METRIK_OPENER_FACILITATOR_NETWORK` | `eip155:84532` | Must match the RPC's chain id. |
343
453
  | `METRIK_OPENER_FACILITATOR_ASSETS` | Circle test USDC | Comma-separated allowlist; must contain `factory.usdc()`. |
@@ -390,6 +500,8 @@ The gateway's normal variables (`METRIK_GATEWAY_ESCROW_ADDRESS`,
390
500
  | `METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS` | required | Stream lifetime. |
391
501
  | `METRIK_GATEWAY_X402V2_PRICE` | required | Base units per paid request (= deposit); `0 < price <= rate × maxDuration`. |
392
502
  | `METRIK_GATEWAY_X402V2_FACTORY_ADDRESS` | the Base Sepolia factory | Required on any other chain. |
503
+ | `METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT` | unset (stream kind) | Set = the upfront kind: the non-refundable per-call part, base units, `<= PRICE`; `PRICE - it <= rate × maxDuration`. |
504
+ | `METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS` | the registry's upfront factory | Only with `_UPFRONT_AMOUNT`. |
393
505
  | `METRIK_GATEWAY_X402V2_NETWORK` | `eip155:<METRIK_GATEWAY_CHAIN_ID>` | Must match the chain id. |
394
506
  | `METRIK_GATEWAY_X402V2_ASSET` / `_ASSET_NAME` / `_ASSET_VERSION` | Circle test USDC / `USDC` / `2` | Token and its EIP-712 domain. |
395
507
  | `METRIK_GATEWAY_X402V2_MAX_TIMEOUT_SECONDS` | `300` | Authorization validity the client signs for. |
@@ -428,7 +540,17 @@ units/s and a price of 60000:
428
540
  > Docs: &lt;docs URL&gt;
429
541
 
430
542
  When the price is below `rate × window`, it adds "(the seller can earn at most
431
- &lt;price&gt;)". `extra.metrikStreamOpener` also carries `refundUrl` (the
543
+ &lt;price&gt;)". An upfront listing (price 0.06, upfront 0.015) states the split:
544
+
545
+ > Pays for one call: 0.06 USDC in total. 0.015 USDC is a NON-REFUNDABLE per-call
546
+ > charge, paid to the seller at settlement whatever is delivered. The remaining
547
+ > 0.045 USDC is metered and refundable: it funds a Metrik escrow stream of …;
548
+ > the seller earns it only for seconds Metrik's verifier confirms the service is
549
+ > delivering, and any unearned part of it (at most 0.045 USDC) is refundable to
550
+ > you (no ETH needed) via …. Refunds are not automatic: …
551
+
552
+ and an upfront-only one says the whole price is charged in full at settlement,
553
+ is not refundable, and that nothing is metered or refundable. `extra.metrikStreamOpener` also carries `refundUrl` (the
432
554
  template) and `docsUrl`; the facilitator accepts exactly these two
433
555
  informational http(s) links on top of the terms, never uses them for
434
556
  settlement, and still refuses any other key. "Verified" here means verified
@@ -457,13 +579,13 @@ protection intact, wrapped by `payX402WithMandate` (`@absol-labs/agent/x402-buye
457
579
  so the package's golden rule holds: **no spend without a mandate**. A Metrik x402
458
580
  payment opens a stream, so it is checked as the stream open it is:
459
581
 
460
- | Mandate cap | x402 quote field |
461
- | ---------------------- | --------------------------------------------- |
462
- | `maxPerStreamUsdc` | `amount` (the deposit, the price of the call) |
463
- | `maxTotalUsdc` | spent so far + `amount` |
464
- | `maxRatePerSecondUsdc` | `ratePerSecond` |
465
- | `maxDurationSeconds` | `maxDuration` |
466
- | `allowedOperators` | `operator` |
582
+ | Mandate cap | x402 quote field |
583
+ | ---------------------- | -------------------------------------- |
584
+ | `maxPerStreamUsdc` | `amount` (the whole price of the call) |
585
+ | `maxTotalUsdc` | spent so far + `amount` |
586
+ | `maxRatePerSecondUsdc` | `ratePerSecond` |
587
+ | `maxDurationSeconds` | `maxDuration` |
588
+ | `allowedOperators` | `operator` |
467
589
 
468
590
  The mandate's signature, expiry, revocation, owner (= the payer) and chain are
469
591
  verified before the seller is contacted. The per-quote check runs in the SDK's
@@ -472,6 +594,17 @@ chain and before anything is signed — and the caps are also passed to the SDK
472
594
  `maxAmount` / `maxRatePerSecond`. A quote without opener terms is refused
473
595
  (`X402MandateDeniedError` reason `not-an-opener-quote`).
474
596
 
597
+ For an upfront quote the mandate counts the WHOLE price, upfront part included,
598
+ as spend (`budgetUsdc = price`); that part is non-recoverable and nothing a
599
+ refund returns is credited back. Its size is also capped separately before
600
+ signing by `maxUpfrontAmount` (`metrik pay --max-upfront`, MCP
601
+ `maxUpfrontAmountUsdc`; default 0.10 USDC, `0` refuses any upfront charge). The
602
+ split is shown before signing and in the result. An upfront-only payment
603
+ (`streamId: null`) is final: `metrik pay` prints no refund or reclaim command,
604
+ `metrik refund 0x00…00` and the MCP refund tools answer "nothing to refund"
605
+ (exit 0 / not an error), and the MCP server records no stream for it — only a
606
+ non-stream charge that still counts against the mandate.
607
+
475
608
  ```bash
476
609
  metrik pay https://x402.137.23.50.249.sslip.io/transform --json '{"text":"# hi"}' --max-amount 0.1
477
610
  metrik refund <streamId> --refund-url <url printed above> --close --reclaim
@@ -525,6 +658,13 @@ refund` and the MCP `refund` tool call it on request; `metrik close` /
525
658
  way out. The relay ledgers are in-memory and per instance, like the
526
659
  settlement ledger; the on-chain contracts still allow only one close and one
527
660
  reclaim per stream regardless.
661
+ - **An upfront charge is final.** With an upfront listing the `upfrontAmount`
662
+ is paid to the seller at settlement whatever is delivered; only `price -
663
+ upfrontAmount` is escrowed. The contract enforces the terms, not the
664
+ marketing text: a buyer's protection is the SDK recomputing `payTo` from the
665
+ advertised six terms and refusing on any difference, plus its
666
+ `maxUpfrontAmount` cap. The listing's signed `commercial` rate band bounds
667
+ only `ratePerSecond`, not the upfront part.
528
668
  - **Stray funds are unrecoverable**: USDC that reaches an opener any way other
529
669
  than `deployAndSettle` / `settleAndOpen` stays there (see the hard rule).
530
670
  - **Blacklist blast radius**: one opener is the escrow buyer for every payer of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absol-labs/agent",
3
- "version": "0.12.0-rc.1",
3
+ "version": "0.12.0-rc.2",
4
4
  "description": "Metrik agent layer: verified-streaming USDC payments metered by delivery, an MCP server, framework tools, and spend mandates so AI agents can hire and pay verified services safely.",
5
5
  "license": "MIT",
6
6
  "author": "Absol Labs",
@@ -85,8 +85,8 @@
85
85
  "pnpm": "9.15.x"
86
86
  },
87
87
  "dependencies": {
88
- "@absol-labs/sdk": "0.10.0-rc.2",
89
- "@absol-labs/shared": "0.14.0-rc.1",
88
+ "@absol-labs/sdk": "0.10.0-rc.3",
89
+ "@absol-labs/shared": "0.14.0-rc.2",
90
90
  "@modelcontextprotocol/sdk": "^1.29.0",
91
91
  "@napi-rs/keyring": "1.3.0",
92
92
  "@privy-io/node": "0.33.0",
@@ -10,6 +10,9 @@ import {
10
10
  resolveCliWallet,
11
11
  } from "../wallet.js";
12
12
  import type { CommandContext, CommandSpec } from "../context.js";
13
+ import type { CliReadClient } from "../deps.js";
14
+ import { escrowGenerationFor } from "../../escrow-generation.js";
15
+ import type { StreamV2 } from "@absol-labs/sdk";
13
16
 
14
17
  /**
15
18
  * The seller's side of the loop.
@@ -65,40 +68,20 @@ export const claimCommand: CommandSpec = {
65
68
  );
66
69
  }
67
70
 
68
- // Accrual lives in the oracle's checkpoint proof, never in the stream tuple.
69
- // No proof means no claim is possible yet, and saying that is better than
70
- // paying gas for a `NothingToClaim()` revert.
71
- let cumulativeAmount: bigint;
72
- try {
73
- cumulativeAmount = (await read.checkpointProof(streamId))
74
- .cumulativeAmount;
75
- } catch (error) {
76
- throw new CliError(
77
- "no checkpoint covers this stream yet, so there is nothing provable to claim",
78
- {
79
- exitCode: 4,
80
- hint: `the oracle publishes roughly every 15-22s and pays only for intervals that verified; ${describeError(
81
- error,
82
- )}`,
83
- cause: error,
84
- },
85
- );
86
- }
87
-
88
- const claimable = await read.claimableForV2(streamId, cumulativeAmount);
89
- if (claimable === 0n) {
90
- throw new CliError("this stream has nothing claimable right now", {
91
- exitCode: 4,
92
- hint: `everything checkpointed so far has already been claimed. See where it stands: metrik status ${streamId}`,
93
- });
94
- }
71
+ const plan = await planClaim(
72
+ read,
73
+ streamId,
74
+ stream,
75
+ escrowGenerationFor(config.chainId, config.escrow),
76
+ );
77
+ if (plan.kind === "nothing") throw plan.error;
95
78
 
96
79
  const write = deps.createWriteClient(
97
80
  config,
98
81
  account,
99
82
  writeClientOptions(wallet),
100
83
  );
101
- const result = await write.claimStream({
84
+ const action = {
102
85
  streamId,
103
86
  // `null` for a policy-enforced wallet — see `../spend-policy.ts`.
104
87
  signedMandate: isPolicyEnforced(wallet)
@@ -106,21 +89,173 @@ export const claimCommand: CommandSpec = {
106
89
  : await signClaimMandate(account, config.chainId),
107
90
  spentSoFarUsdc: 0n,
108
91
  nowSeconds: deps.nowSeconds(),
109
- });
92
+ };
93
+ let result: { readonly txHash: `0x${string}` };
94
+ if (plan.kind === "claimWithProof") {
95
+ result = await write.claimStream(action);
96
+ } else {
97
+ if (write.claimProvenStream === undefined) {
98
+ throw new CliError(
99
+ "this write client cannot send claimProven, which is the only way to collect this stream's proven value",
100
+ {
101
+ exitCode: 3,
102
+ hint: "no current checkpoint proof pays it, so a proof claim would revert NothingToClaim",
103
+ },
104
+ );
105
+ }
106
+ result = await write.claimProvenStream(action);
107
+ }
110
108
 
111
109
  out.result({
112
110
  streamId,
113
111
  txHash: result.txHash,
114
112
  operator: stream.operator,
115
- claimed: claimable,
116
- verifiedCumulative: cumulativeAmount,
113
+ claimed: plan.claimable,
114
+ method: plan.kind,
115
+ verifiedCumulative: plan.verifiedCumulative,
116
+ provenCumulative: plan.provenCumulative,
117
117
  });
118
118
  out.print("Claimed.");
119
119
  out.table([
120
120
  ["streamId", streamId],
121
121
  ["tx", result.txHash],
122
122
  ["operator", stream.operator],
123
- ["claimed", `${formatUsdc(claimable)} (net of protocol fee)`],
123
+ ["claimed", `${formatUsdc(plan.claimable)} (net of protocol fee)`],
124
+ [
125
+ "via",
126
+ plan.kind === "claimWithProof"
127
+ ? "claimWithProof (the oracle's current checkpoint proof)"
128
+ : `claimProven (the on-chain proven high-water, ${formatUsdc(plan.provenCumulative ?? 0n)} gross — no current checkpoint proof pays more)`,
129
+ ],
124
130
  ]);
125
131
  },
126
132
  };
133
+
134
+ /**
135
+ * Which claim, if any, `metrik claim` would send. Reads only, so it is
136
+ * testable (and checkable against a live stream) without a key.
137
+ *
138
+ * - `claimWithProof` pays `min(proofCumulative, deposit) − claimed` against ANY
139
+ * retained checkpoint root.
140
+ * - `claimProven` (StreamEscrowV3) pays `min(provenCumulative, deposit) −
141
+ * claimed` with NO proof: the high-water was itself proven against a signed
142
+ * root when it was recorded. It is the only way to collect proven value whose
143
+ * root has rotated out of the retained set, or that a later root regressed.
144
+ *
145
+ * Both are asked of the escrow (`claimableFor` at each cumulative; the escrow
146
+ * pays a claim with exactly that math), and the larger is chosen. A proof
147
+ * claim wins a tie, because it also raises the proven high-water. Nothing is
148
+ * chosen when both pay zero, so no gas is spent to learn `NothingToClaim`.
149
+ */
150
+ export type ClaimPlan =
151
+ | {
152
+ readonly kind: "claimWithProof" | "claimProven";
153
+ /** What the operator receives, net of the protocol fee. */
154
+ readonly claimable: bigint;
155
+ /** The current proof's cumulative; `null` when no proof was available. */
156
+ readonly verifiedCumulative: bigint | null;
157
+ /** V3 proven high-water; `null` on V2 or when unreadable. */
158
+ readonly provenCumulative: bigint | null;
159
+ }
160
+ | { readonly kind: "nothing"; readonly error: CliError };
161
+
162
+ export async function planClaim(
163
+ read: CliReadClient,
164
+ streamId: `0x${string}`,
165
+ stream: Pick<StreamV2, "claimedCumulative">,
166
+ escrowGeneration: string,
167
+ ): Promise<ClaimPlan> {
168
+ let verifiedCumulative: bigint | null = null;
169
+ let proofError: unknown = null;
170
+ try {
171
+ verifiedCumulative = (await read.checkpointProof(streamId))
172
+ .cumulativeAmount;
173
+ } catch (error) {
174
+ proofError = error;
175
+ }
176
+
177
+ let provenCumulative: bigint | null = null;
178
+ let provenError: string | null = null;
179
+ if (escrowGeneration === "v3") {
180
+ if (read.provenCumulativeOf === undefined) {
181
+ provenError = "this read client cannot read provenCumulativeOf";
182
+ } else {
183
+ try {
184
+ provenCumulative = await read.provenCumulativeOf(streamId);
185
+ } catch (error) {
186
+ provenError = describeError(error);
187
+ }
188
+ }
189
+ }
190
+
191
+ const [proofClaimable, provenClaimable] = await Promise.all([
192
+ verifiedCumulative === null
193
+ ? 0n
194
+ : read.claimableForV2(streamId, verifiedCumulative),
195
+ // `claimProven` pays at the high-water, so it is worth asking only when
196
+ // the high-water is above what has already been claimed.
197
+ provenCumulative === null || provenCumulative <= stream.claimedCumulative
198
+ ? 0n
199
+ : read.claimableForV2(streamId, provenCumulative),
200
+ ]);
201
+
202
+ if (proofClaimable > 0n && proofClaimable >= provenClaimable) {
203
+ return {
204
+ kind: "claimWithProof",
205
+ claimable: proofClaimable,
206
+ verifiedCumulative,
207
+ provenCumulative,
208
+ };
209
+ }
210
+ if (provenClaimable > 0n) {
211
+ return {
212
+ kind: "claimProven",
213
+ claimable: provenClaimable,
214
+ verifiedCumulative,
215
+ provenCumulative,
216
+ };
217
+ }
218
+
219
+ if (provenCumulative !== null && provenCumulative > 0n) {
220
+ return {
221
+ kind: "nothing",
222
+ error: new CliError("this stream has nothing to claim", {
223
+ exitCode: 4,
224
+ hint: `everything proven on-chain for it (${formatUsdc(provenCumulative)} gross) has already been claimed${
225
+ verifiedCumulative === null
226
+ ? ", and the latest checkpoint no longer contains it"
227
+ : ""
228
+ }. See where it stands: metrik status ${streamId}`,
229
+ }),
230
+ };
231
+ }
232
+ if (verifiedCumulative === null) {
233
+ // Accrual lives in the oracle's checkpoint proof (and, on V3, the proven
234
+ // high-water), never in the stream tuple. Saying so is better than paying
235
+ // gas for a `NothingToClaim()` revert.
236
+ return {
237
+ kind: "nothing",
238
+ error: new CliError(
239
+ "no checkpoint covers this stream yet, so there is nothing provable to claim",
240
+ {
241
+ exitCode: 4,
242
+ hint: `the oracle publishes roughly every 15-22s and pays only for intervals that verified; ${describeError(
243
+ proofError,
244
+ )}${
245
+ provenError === null
246
+ ? ""
247
+ : `; the on-chain proven high-water could not be read either (${provenError})`
248
+ }`,
249
+ cause: proofError,
250
+ },
251
+ ),
252
+ };
253
+ }
254
+ return {
255
+ kind: "nothing",
256
+ error: new CliError("this stream has nothing claimable right now", {
257
+ exitCode: 4,
258
+ hint: `everything checkpointed so far has already been claimed. See where it stands: metrik status ${streamId}`,
259
+ }),
260
+ };
261
+ }