@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.
- package/README.md +1 -1
- package/dist/cli/commands/claim.d.ts +32 -0
- package/dist/cli/commands/claim.d.ts.map +1 -1
- package/dist/cli/commands/claim.js +115 -27
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/pay.d.ts +15 -0
- package/dist/cli/commands/pay.d.ts.map +1 -1
- package/dist/cli/commands/pay.js +131 -7
- package/dist/cli/commands/pay.js.map +1 -1
- package/dist/cli/commands/reclaim.d.ts.map +1 -1
- package/dist/cli/commands/reclaim.js +20 -4
- package/dist/cli/commands/reclaim.js.map +1 -1
- package/dist/cli/commands/refund.d.ts +19 -1
- package/dist/cli/commands/refund.d.ts.map +1 -1
- package/dist/cli/commands/refund.js +200 -27
- package/dist/cli/commands/refund.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +11 -1
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js +2 -0
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli/context.d.ts +19 -0
- package/dist/cli/context.d.ts.map +1 -1
- package/dist/cli/deps.d.ts +20 -0
- package/dist/cli/deps.d.ts.map +1 -1
- package/dist/cli/deps.js +14 -0
- package/dist/cli/deps.js.map +1 -1
- package/dist/cli/progress.d.ts +13 -1
- package/dist/cli/progress.d.ts.map +1 -1
- package/dist/cli/progress.js +7 -1
- package/dist/cli/progress.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +26 -14
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +103 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +310 -41
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/wallet.d.ts +1 -1
- package/dist/cli/wallet.d.ts.map +1 -1
- package/dist/cli/wallet.js +28 -4
- package/dist/cli/wallet.js.map +1 -1
- package/dist/gateway/http-server.d.ts +17 -0
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +76 -9
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/x402-opener-mode.d.ts +39 -2
- package/dist/gateway/x402-opener-mode.d.ts.map +1 -1
- package/dist/gateway/x402-opener-mode.js +169 -36
- package/dist/gateway/x402-opener-mode.js.map +1 -1
- package/dist/mcp/server.d.ts +12 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +28 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/x402-tools.d.ts +8 -3
- package/dist/mcp/x402-tools.d.ts.map +1 -1
- package/dist/mcp/x402-tools.js +126 -6
- package/dist/mcp/x402-tools.js.map +1 -1
- package/dist/sdk/client.d.ts +13 -0
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +15 -0
- package/dist/sdk/client.js.map +1 -1
- package/dist/x402-buyer/pay.d.ts +22 -0
- package/dist/x402-buyer/pay.d.ts.map +1 -1
- package/dist/x402-buyer/pay.js +33 -3
- package/dist/x402-buyer/pay.js.map +1 -1
- package/dist/x402-opener/facilitator-entry.js +4 -1
- package/dist/x402-opener/facilitator-entry.js.map +1 -1
- package/dist/x402-opener/facilitator-server.d.ts +29 -0
- package/dist/x402-opener/facilitator-server.d.ts.map +1 -1
- package/dist/x402-opener/facilitator-server.js +67 -1
- package/dist/x402-opener/facilitator-server.js.map +1 -1
- package/dist/x402-opener/refund-relay.d.ts +30 -4
- package/dist/x402-opener/refund-relay.d.ts.map +1 -1
- package/dist/x402-opener/refund-relay.js +123 -22
- package/dist/x402-opener/refund-relay.js.map +1 -1
- package/dist/x402-opener/scheme.d.ts +79 -0
- package/dist/x402-opener/scheme.d.ts.map +1 -1
- package/dist/x402-opener/scheme.js +326 -40
- package/dist/x402-opener/scheme.js.map +1 -1
- package/dist/x402-opener/terms.d.ts +61 -1
- package/dist/x402-opener/terms.d.ts.map +1 -1
- package/dist/x402-opener/terms.js +94 -2
- package/dist/x402-opener/terms.js.map +1 -1
- package/docs/cli.md +87 -9
- package/docs/mcp-reference.md +18 -2
- package/docs/quickstart.md +3 -3
- package/docs/x402.md +150 -10
- package/package.json +3 -3
- package/src/cli/commands/claim.ts +167 -32
- package/src/cli/commands/pay.ts +150 -8
- package/src/cli/commands/reclaim.ts +25 -5
- package/src/cli/commands/refund.ts +231 -33
- package/src/cli/commands/status.ts +11 -1
- package/src/cli/commands/watch.ts +4 -0
- package/src/cli/context.ts +16 -0
- package/src/cli/deps.ts +37 -0
- package/src/cli/progress.ts +21 -2
- package/src/cli/run.ts +26 -13
- package/src/cli/status.ts +440 -43
- package/src/cli/wallet.ts +44 -7
- package/src/gateway/http-server.ts +96 -8
- package/src/gateway/x402-opener-mode.ts +234 -37
- package/src/mcp/server.ts +43 -11
- package/src/mcp/x402-tools.ts +171 -7
- package/src/sdk/client.ts +25 -0
- package/src/x402-buyer/pay.ts +41 -3
- package/src/x402-opener/facilitator-entry.ts +4 -1
- package/src/x402-opener/facilitator-server.ts +101 -1
- package/src/x402-opener/refund-relay.ts +150 -20
- package/src/x402-opener/scheme.ts +459 -41
- package/src/x402-opener/terms.ts +155 -2
package/docs/quickstart.md
CHANGED
|
@@ -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:
|
|
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:
|
|
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.
|
|
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: <docs URL>
|
|
429
541
|
|
|
430
542
|
When the price is below `rate × window`, it adds "(the seller can earn at most
|
|
431
|
-
<price>)".
|
|
543
|
+
<price>)". 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
|
|
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.
|
|
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.
|
|
89
|
-
"@absol-labs/shared": "0.14.0-rc.
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|