@absol-labs/agent 0.12.0-rc.3 → 0.12.0-rc.4
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/dist/cli/commands/claim.d.ts +6 -0
- package/dist/cli/commands/claim.d.ts.map +1 -1
- package/dist/cli/commands/claim.js +53 -22
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/pay.d.ts +10 -0
- package/dist/cli/commands/pay.d.ts.map +1 -1
- package/dist/cli/commands/pay.js +246 -50
- package/dist/cli/commands/pay.js.map +1 -1
- package/dist/cli/commands/refund.d.ts +21 -12
- package/dist/cli/commands/refund.d.ts.map +1 -1
- package/dist/cli/commands/refund.js +61 -206
- package/dist/cli/commands/refund.js.map +1 -1
- package/dist/cli/deps.d.ts +6 -0
- package/dist/cli/deps.d.ts.map +1 -1
- package/dist/cli/deps.js +3 -0
- package/dist/cli/deps.js.map +1 -1
- package/dist/cli/errors.d.ts +8 -1
- package/dist/cli/errors.d.ts.map +1 -1
- package/dist/cli/errors.js.map +1 -1
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +21 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/x402-tools.d.ts +7 -1
- package/dist/mcp/x402-tools.d.ts.map +1 -1
- package/dist/mcp/x402-tools.js +159 -6
- package/dist/mcp/x402-tools.js.map +1 -1
- package/dist/x402-buyer/index.d.ts +1 -0
- package/dist/x402-buyer/index.d.ts.map +1 -1
- package/dist/x402-buyer/index.js +1 -0
- package/dist/x402-buyer/index.js.map +1 -1
- package/dist/x402-buyer/pay.d.ts +38 -2
- package/dist/x402-buyer/pay.d.ts.map +1 -1
- package/dist/x402-buyer/pay.js +52 -2
- package/dist/x402-buyer/pay.js.map +1 -1
- package/dist/x402-buyer/refund-actions.d.ts +168 -0
- package/dist/x402-buyer/refund-actions.d.ts.map +1 -0
- package/dist/x402-buyer/refund-actions.js +351 -0
- package/dist/x402-buyer/refund-actions.js.map +1 -0
- package/docs/cli.md +39 -7
- package/docs/mcp-reference.md +23 -0
- package/docs/x402.md +43 -3
- package/package.json +1 -1
- package/src/cli/commands/claim.ts +55 -29
- package/src/cli/commands/pay.ts +299 -51
- package/src/cli/commands/refund.ts +89 -240
- package/src/cli/deps.ts +9 -0
- package/src/cli/errors.ts +8 -1
- package/src/mcp/server.ts +30 -1
- package/src/mcp/x402-tools.ts +222 -7
- package/src/x402-buyer/index.ts +1 -0
- package/src/x402-buyer/pay.ts +99 -2
- package/src/x402-buyer/refund-actions.ts +486 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The buyer's own close + reclaim of an x402 stream, through the seller's
|
|
3
|
+
* gasless refund relay. ONE implementation behind `metrik refund`,
|
|
4
|
+
* `metrik pay --close-after`, the MCP `x402_pay` tool's `closeAfter` and
|
|
5
|
+
* {@link closeAfterPayment} (`@absol-labs/agent/x402-buyer`).
|
|
6
|
+
*
|
|
7
|
+
* Nothing here is an automatic refund: it runs only when the buyer asks for
|
|
8
|
+
* it, and it is the buyer's own client that signs the Close and asks the
|
|
9
|
+
* relay to send the permissionless reclaim. The relay pays the gas; the
|
|
10
|
+
* opener always sends the reclaim to the payer.
|
|
11
|
+
*
|
|
12
|
+
* The waits are bounded and the relay's own: `RefundClient.status()` waits
|
|
13
|
+
* out a `503 chain-lagging` (honouring `Retry-After`), and
|
|
14
|
+
* `RefundClient.reclaim()` retries, bounded by the client's `maxAttempts` /
|
|
15
|
+
* `maxDelayMs`, while the relay says the checkpoint covering the close is not
|
|
16
|
+
* posted yet.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* After a close/reclaim, how many times the summary re-reads the relay until
|
|
20
|
+
* it reflects the transaction just mined (a load-balanced RPC behind the relay
|
|
21
|
+
* can answer from a node that has not seen it yet), and how long it waits
|
|
22
|
+
* between reads. Bounded: past it the summary is derived from the verified
|
|
23
|
+
* receipts instead.
|
|
24
|
+
*/
|
|
25
|
+
export const POST_ACTION_READS = 6;
|
|
26
|
+
export const POST_ACTION_INTERVAL_MS = 1_500;
|
|
27
|
+
/**
|
|
28
|
+
* A close or reclaim that did not complete. `closed` says whether the stream
|
|
29
|
+
* is known to be closed (by this run or before it), so the caller can name the
|
|
30
|
+
* exact command that finishes the job. `cause` is the SDK's error, unchanged.
|
|
31
|
+
*/
|
|
32
|
+
export class RefundActionsError extends Error {
|
|
33
|
+
stage;
|
|
34
|
+
closed;
|
|
35
|
+
closeResult;
|
|
36
|
+
constructor(context) {
|
|
37
|
+
super(`the ${context.stage} did not finish: ${context.cause instanceof Error ? context.cause.message : String(context.cause)}`, { cause: context.cause });
|
|
38
|
+
this.name = "RefundActionsError";
|
|
39
|
+
this.stage = context.stage;
|
|
40
|
+
this.closed = context.closed;
|
|
41
|
+
this.closeResult = context.closeResult;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Close and/or reclaim one x402 stream through its refund relay, then read
|
|
46
|
+
* the state AFTER those actions. Finished states (already closed, already
|
|
47
|
+
* reclaimed, nothing to reclaim) are reported, not thrown. Anything else that
|
|
48
|
+
* fails is a {@link RefundActionsError} carrying the SDK's error as `cause`.
|
|
49
|
+
*/
|
|
50
|
+
export async function runRefundActions(input) {
|
|
51
|
+
const { refunds } = input;
|
|
52
|
+
let closeResult = null;
|
|
53
|
+
let closeSkipped = null;
|
|
54
|
+
// Set when the relay itself says there is nothing left: the stream was
|
|
55
|
+
// already reclaimed, or the verified delivery covers the whole deposit.
|
|
56
|
+
// Those are FINISHED recoveries, not failures — a second close + reclaim on
|
|
57
|
+
// a done stream must succeed and never point the payer at a gas-spending
|
|
58
|
+
// on-chain recovery that cannot recover a cent.
|
|
59
|
+
let finishedByRelay = null;
|
|
60
|
+
if (input.close) {
|
|
61
|
+
try {
|
|
62
|
+
// Ask first: a stream that is already closed (or already finished)
|
|
63
|
+
// needs no Close, so there is nothing to sign and no wallet is needed.
|
|
64
|
+
const before = await refunds.status();
|
|
65
|
+
const finishedBefore = finishedReason(before);
|
|
66
|
+
if (finishedBefore !== null || before.status === "closed") {
|
|
67
|
+
closeSkipped = finishedBefore ?? "already-closed";
|
|
68
|
+
finishedByRelay = finishedBefore;
|
|
69
|
+
input.note?.(finishedBefore === null
|
|
70
|
+
? "stream is already closed — no Close to sign."
|
|
71
|
+
: `stream is already closed and ${finishedBefore === "reclaimed" ? "reclaimed" : "has nothing left to reclaim"} — no Close to sign.`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const signer = await input.closeSigner();
|
|
75
|
+
try {
|
|
76
|
+
closeResult = await refunds.close({ signer });
|
|
77
|
+
input.note?.(`close relayed in ${closeResult.transaction}${closeResult.verified ? " (receipt verified on-chain)" : ""}`);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
// The status read above said "active", but the stream closed in the
|
|
81
|
+
// meantime (it reached expiresAt, or another close landed): the
|
|
82
|
+
// state the payer asked for. Anything else is a real failure.
|
|
83
|
+
if (relayCode(error) !== "already-closed")
|
|
84
|
+
throw error;
|
|
85
|
+
closeSkipped = "already-closed";
|
|
86
|
+
input.note?.("stream was already closed — no Close was needed.");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new RefundActionsError({
|
|
92
|
+
stage: "close",
|
|
93
|
+
closed: false,
|
|
94
|
+
closeResult: null,
|
|
95
|
+
cause: error,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let reclaimResult = null;
|
|
100
|
+
let reclaimSkipped = null;
|
|
101
|
+
if (input.reclaim) {
|
|
102
|
+
if (finishedByRelay !== null) {
|
|
103
|
+
reclaimSkipped = finishedByRelay;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
if (input.reclaimActivity !== undefined) {
|
|
107
|
+
input.setActivity?.(input.reclaimActivity);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
reclaimResult = await refunds.reclaim();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const finished = finishedFromRelayCode(relayCode(error));
|
|
114
|
+
if (finished === null) {
|
|
115
|
+
throw new RefundActionsError({
|
|
116
|
+
stage: "reclaim",
|
|
117
|
+
closed: closeResult !== null || closeSkipped !== null,
|
|
118
|
+
closeResult,
|
|
119
|
+
cause: error,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
reclaimSkipped = finished;
|
|
123
|
+
finishedByRelay = finished;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
input.setActivity?.(null);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// The summary describes the state AFTER what this run did: re-read until
|
|
131
|
+
// the relay reflects the close/reclaim just mined (bounded), else derive it
|
|
132
|
+
// from the verified receipts. Never the pre-action state.
|
|
133
|
+
const status = await statusAfterActions(refunds, {
|
|
134
|
+
closed: closeResult !== null,
|
|
135
|
+
reclaim: reclaimResult,
|
|
136
|
+
payer: input.payer,
|
|
137
|
+
opener: input.opener,
|
|
138
|
+
streamId: input.streamId,
|
|
139
|
+
sleep: input.sleep,
|
|
140
|
+
});
|
|
141
|
+
// A reclaim THIS run performed is reported as a reclaim, not as "already
|
|
142
|
+
// finished".
|
|
143
|
+
const finished = reclaimResult === null ? (finishedReason(status) ?? finishedByRelay) : null;
|
|
144
|
+
return {
|
|
145
|
+
close: closeResult,
|
|
146
|
+
closeSkipped,
|
|
147
|
+
reclaim: reclaimResult,
|
|
148
|
+
reclaimSkipped,
|
|
149
|
+
status,
|
|
150
|
+
finished,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Right after a paid response, at the BUYER's request: close the stream the
|
|
155
|
+
* payment opened (the payer signs the Close, the relay pays the gas), wait for
|
|
156
|
+
* the checkpoint covering the close, and reclaim the unearned remainder to the
|
|
157
|
+
* payer. Never throws for a close/reclaim failure: the payment already
|
|
158
|
+
* succeeded, so the outcome says what did not finish instead.
|
|
159
|
+
*
|
|
160
|
+
* An upfront-only payment (no stream) has nothing to close: the outcome is
|
|
161
|
+
* `nothing-to-close` and nothing is requested, signed or sent.
|
|
162
|
+
*/
|
|
163
|
+
export async function closeAfterPayment(input) {
|
|
164
|
+
const { payment } = input;
|
|
165
|
+
const empty = {
|
|
166
|
+
close: null,
|
|
167
|
+
closeSkipped: null,
|
|
168
|
+
reclaim: null,
|
|
169
|
+
reclaimSkipped: null,
|
|
170
|
+
refundedUsdc: null,
|
|
171
|
+
finished: null,
|
|
172
|
+
status: null,
|
|
173
|
+
};
|
|
174
|
+
if (payment.streamId === null || /^0x0{64}$/i.test(payment.streamId)) {
|
|
175
|
+
return {
|
|
176
|
+
...empty,
|
|
177
|
+
outcome: "nothing-to-close",
|
|
178
|
+
streamId: null,
|
|
179
|
+
incomplete: null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const streamId = payment.streamId;
|
|
183
|
+
if (payment.refundUrl === null) {
|
|
184
|
+
return {
|
|
185
|
+
...empty,
|
|
186
|
+
outcome: "incomplete",
|
|
187
|
+
streamId,
|
|
188
|
+
incomplete: {
|
|
189
|
+
stage: "no-relay",
|
|
190
|
+
closed: false,
|
|
191
|
+
reason: "the seller advertised no refund relay, so there is no gasless close; the stream accrues only for verified seconds until it expires, and anyone can reclaim the unearned remainder to the payer after that",
|
|
192
|
+
error: null,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
let refunds;
|
|
197
|
+
try {
|
|
198
|
+
refunds = await input.openRefunds();
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
return {
|
|
202
|
+
...empty,
|
|
203
|
+
outcome: "incomplete",
|
|
204
|
+
streamId,
|
|
205
|
+
incomplete: {
|
|
206
|
+
stage: "close",
|
|
207
|
+
closed: false,
|
|
208
|
+
reason: errorMessage(error),
|
|
209
|
+
error,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const result = await runRefundActions({
|
|
215
|
+
...input,
|
|
216
|
+
refunds,
|
|
217
|
+
streamId,
|
|
218
|
+
opener: payment.opener,
|
|
219
|
+
close: true,
|
|
220
|
+
reclaim: true,
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
outcome: "completed",
|
|
224
|
+
streamId,
|
|
225
|
+
close: result.close,
|
|
226
|
+
closeSkipped: result.closeSkipped,
|
|
227
|
+
reclaim: result.reclaim,
|
|
228
|
+
reclaimSkipped: result.reclaimSkipped,
|
|
229
|
+
refundedUsdc: result.reclaim !== null && result.reclaim.verified
|
|
230
|
+
? result.reclaim.amount
|
|
231
|
+
: null,
|
|
232
|
+
finished: result.finished,
|
|
233
|
+
status: result.status,
|
|
234
|
+
incomplete: null,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (!(error instanceof RefundActionsError))
|
|
239
|
+
throw error;
|
|
240
|
+
return {
|
|
241
|
+
...empty,
|
|
242
|
+
outcome: "incomplete",
|
|
243
|
+
streamId,
|
|
244
|
+
close: error.closeResult,
|
|
245
|
+
incomplete: {
|
|
246
|
+
stage: error.stage,
|
|
247
|
+
closed: error.closed,
|
|
248
|
+
reason: errorMessage(error.cause),
|
|
249
|
+
error: error.cause,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function errorMessage(error) {
|
|
255
|
+
return error instanceof Error ? error.message : String(error);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The relay's view of a stream AFTER this run's actions. Reads until it
|
|
259
|
+
* reflects them (closed after a close, reclaimed after a reclaim), bounded;
|
|
260
|
+
* if the relay still lags, the state those verified transactions imply is
|
|
261
|
+
* reported instead, marked as derived. With no action, one plain read.
|
|
262
|
+
*/
|
|
263
|
+
export async function statusAfterActions(refunds, done) {
|
|
264
|
+
const reclaimed = done.reclaim !== null;
|
|
265
|
+
if (!done.closed && !reclaimed)
|
|
266
|
+
return await refunds.status();
|
|
267
|
+
const reflects = (status) => (!done.closed || status.status === "closed" || status.reclaimed) &&
|
|
268
|
+
(!reclaimed || status.reclaimed);
|
|
269
|
+
let last = null;
|
|
270
|
+
for (let attempt = 1; attempt <= POST_ACTION_READS; attempt++) {
|
|
271
|
+
try {
|
|
272
|
+
const status = await refunds.status();
|
|
273
|
+
if (reflects(status))
|
|
274
|
+
return status;
|
|
275
|
+
last = status;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// A lagging relay (503 chain-lagging, a stale not-found, a blip): the
|
|
279
|
+
// transactions already happened, so keep going and derive if need be.
|
|
280
|
+
}
|
|
281
|
+
if (attempt < POST_ACTION_READS)
|
|
282
|
+
await done.sleep(POST_ACTION_INTERVAL_MS);
|
|
283
|
+
}
|
|
284
|
+
return derivedStatus(last, done);
|
|
285
|
+
}
|
|
286
|
+
/** The state this run's verified close/reclaim imply, when the relay lags. */
|
|
287
|
+
function derivedStatus(last, done) {
|
|
288
|
+
const reclaimed = done.reclaim !== null;
|
|
289
|
+
const note = "derived from this run's transaction receipts; the refund relay had not caught up with them yet";
|
|
290
|
+
return {
|
|
291
|
+
streamId: done.streamId,
|
|
292
|
+
chainId: last?.chainId ?? 0,
|
|
293
|
+
opener: last?.opener ?? done.opener,
|
|
294
|
+
payer: last?.payer ?? done.payer ?? done.opener,
|
|
295
|
+
status: "closed",
|
|
296
|
+
reclaimed: reclaimed || (last?.reclaimed ?? false),
|
|
297
|
+
chainTime: last?.chainTime ?? null,
|
|
298
|
+
reclaimable: null,
|
|
299
|
+
// Both legs this run verified are on-chain facts: the stream is closed.
|
|
300
|
+
state: "closed",
|
|
301
|
+
onChainStatus: "closed",
|
|
302
|
+
maxRefund: last?.maxRefund ?? null,
|
|
303
|
+
maxRefundMeaning: last?.maxRefundMeaning ?? null,
|
|
304
|
+
reclaimableMeaning: last?.reclaimableMeaning ?? null,
|
|
305
|
+
next: reclaimed
|
|
306
|
+
? {
|
|
307
|
+
action: "none",
|
|
308
|
+
message: "Reclaimed: the unearned remainder was returned to the payer. Nothing further to do.",
|
|
309
|
+
}
|
|
310
|
+
: {
|
|
311
|
+
action: "reclaim",
|
|
312
|
+
message: "Closed. Reclaim the unearned remainder once a checkpoint covering the close is posted (usually within a minute).",
|
|
313
|
+
},
|
|
314
|
+
raw: { derived: true, note },
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Whether the relay's status says nothing is left to recover. `reclaimed` is
|
|
319
|
+
* the chain's one-reclaim-per-stream flag; `next: none` with `reclaimable: 0`
|
|
320
|
+
* is the relay's "the verified delivery covers the whole deposit".
|
|
321
|
+
*/
|
|
322
|
+
export function finishedReason(status) {
|
|
323
|
+
if (status.reclaimed)
|
|
324
|
+
return "reclaimed";
|
|
325
|
+
if (status.next.action === "none" && status.reclaimable === 0n) {
|
|
326
|
+
return "nothing-to-reclaim";
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
/** The relay's 409 codes that mean "finished", not "failed". */
|
|
331
|
+
function finishedFromRelayCode(code) {
|
|
332
|
+
if (code === "already-reclaimed")
|
|
333
|
+
return "reclaimed";
|
|
334
|
+
if (code === "nothing-to-reclaim")
|
|
335
|
+
return "nothing-to-reclaim";
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
/** The relay's refusal code, looked up through a few `cause` links. */
|
|
339
|
+
export function relayCode(error) {
|
|
340
|
+
for (let current = error, depth = 0; depth < 4; depth++) {
|
|
341
|
+
if (!(current instanceof Error))
|
|
342
|
+
return null;
|
|
343
|
+
const code = current.code;
|
|
344
|
+
if (current.name === "RefundRelayError" && typeof code === "string") {
|
|
345
|
+
return code;
|
|
346
|
+
}
|
|
347
|
+
current = current.cause;
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=refund-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refund-actions.js","sourceRoot":"","sources":["../../src/x402-buyer/refund-actions.ts"],"names":[],"mappings":"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"}
|
package/docs/cli.md
CHANGED
|
@@ -605,9 +605,10 @@ everyone who ran it. It is left out until that constraint lifts.
|
|
|
605
605
|
|
|
606
606
|
```bash
|
|
607
607
|
metrik pay https://x402.example/transform --json '{"text":"# hi"}' --max-amount 0.1
|
|
608
|
+
metrik pay https://x402.example/transform --json '{"text":"# hi"}' --max-amount 0.1 --close-after
|
|
608
609
|
metrik pay <url> [--json <body> | --data <body>] [--method POST] [--header "K: V"] \
|
|
609
610
|
--max-amount <usdc> [--max-rate <usdc/s>] [--max-upfront <usdc>] [--max-duration <s>] \
|
|
610
|
-
[--min-duration <s>] [--operator <address>] [--units atomic]
|
|
611
|
+
[--min-duration <s>] [--operator <address>] [--close-after] [--units atomic]
|
|
611
612
|
```
|
|
612
613
|
|
|
613
614
|
Pays any **x402 v2 `exact`** endpoint through `@absol-labs/sdk/x402`'s `payWithX402`,
|
|
@@ -679,11 +680,41 @@ facilitator's `PAYMENT-RESPONSE` header (relayed by the seller) carries the sett
|
|
|
679
680
|
transaction hash, it is printed as `settlement tx` and returned as
|
|
680
681
|
`settlementTransaction` with `settlementTransactionSource: "PAYMENT-RESPONSE"`: the
|
|
681
682
|
facilitator's relayer broadcast it, not the payer, and the CLI reports it as given.
|
|
682
|
-
**Refunds are not automatic** — see `metrik refund
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
683
|
+
**Refunds are not automatic** — see `metrik refund`, or ask for the close up front with
|
|
684
|
+
`--close-after`.
|
|
685
|
+
|
|
686
|
+
**Close right after the response (`--close-after`).** The metered part of a payment pays
|
|
687
|
+
per verified second of the seller's uptime, not per call: a one-shot caller who never
|
|
688
|
+
closes lets the seller earn the whole metered part for a single response. With
|
|
689
|
+
`--close-after`, once the paid response is in (the request settled), the CLI does what
|
|
690
|
+
`metrik refund <streamId> --refund-url <url> --close --reclaim` does, in the same run and
|
|
691
|
+
with the same code: it signs the payer's `Close` (the payer key, no gas), the seller's
|
|
692
|
+
refund relay sends it and pays the gas, the CLI waits for the checkpoint covering the close
|
|
693
|
+
(bounded: up to 5 relay attempts, at most 60 s each, honouring `Retry-After`; a relay that
|
|
694
|
+
lags the chain is waited out the same way), then asks the relay to reclaim the unearned
|
|
695
|
+
part to the payer. This is your client acting on your request — the escrow does not
|
|
696
|
+
return anything by itself, and nothing is sent unless you pass the flag. It then prints
|
|
697
|
+
what came back, from the verified receipt: `close tx`, `reclaim tx`, `refunded to payer`,
|
|
698
|
+
`seller earned` (verified seconds) and `payment cost` (price minus refund: the upfront
|
|
699
|
+
part plus what was earned). JSON adds `closeAfter` (`outcome`, `close`, `reclaim`,
|
|
700
|
+
`refunded`, `kept`, `earned`, `finishCommand`, `sentBy: "buyer"`, `refundIsAutomatic:
|
|
701
|
+
false`). An upfront-only payment has nothing to close: it says so and exits `0` with
|
|
702
|
+
`closeAfter.outcome: "nothing-to-close"`. A request that did not settle (nothing paid)
|
|
703
|
+
closes nothing (`closeAfter.outcome: "not-paid"`).
|
|
704
|
+
|
|
705
|
+
If the close or the reclaim does not finish (the relay is down, the checkpoint is late,
|
|
706
|
+
the seller advertised no relay), the payment and the response still stand and nothing is
|
|
707
|
+
lost — the unearned part stays in the escrow for the payer. `pay` then exits **`5`**,
|
|
708
|
+
keeps the full result on stdout (`closeAfter.outcome: "incomplete"`, with the `stage` that
|
|
709
|
+
did not finish), and prints the exact command that finishes it, for example
|
|
710
|
+
`metrik refund <streamId> --refund-url <url> --reclaim` when the stream is already closed.
|
|
711
|
+
|
|
712
|
+
Exit codes: `0` paid (or nothing to pay), `2` usage (including no ceiling), `3` refused by
|
|
713
|
+
the mandate or the SDK (nothing signed, including an upfront part above `--max-upfront`),
|
|
714
|
+
`4` the settlement failed after signing (the error says whether the authorization was used
|
|
715
|
+
and how to cancel it; see [x402.md](./x402.md)), `5` paid and answered, but the
|
|
716
|
+
`--close-after` close/reclaim did not finish (the hint names the command that finishes
|
|
717
|
+
it).
|
|
687
718
|
|
|
688
719
|
### `metrik refund`
|
|
689
720
|
|
|
@@ -906,7 +937,8 @@ done
|
|
|
906
937
|
```
|
|
907
938
|
|
|
908
939
|
Exit codes: `0` success, `2` usage error, `3` refused configuration (wrong chain, no
|
|
909
|
-
wallet, mandate denied), `4` the operation failed
|
|
940
|
+
wallet, mandate denied), `4` the operation failed, `5` (`metrik pay --close-after` only)
|
|
941
|
+
paid and answered, but the close/reclaim did not finish.
|
|
910
942
|
|
|
911
943
|
**Nothing waits silently.** Every RPC call carries a 15s budget with two bounded retries,
|
|
912
944
|
and the oracle fetch — an HTTP call the RPC transport does not cover — has its own 20s
|
package/docs/mcp-reference.md
CHANGED
|
@@ -193,6 +193,29 @@ the stream record carries `nonRefundableUsdc`. An **upfront-only** payment
|
|
|
193
193
|
reservation) is recorded — only a non-stream charge that keeps counting against
|
|
194
194
|
`maxTotalUsdc` and shows in `list_streams` as such — and the payment is final.
|
|
195
195
|
|
|
196
|
+
**`closeAfter: true` — close right after the response.** The metered part pays per
|
|
197
|
+
verified second of the seller's uptime, not per call, so a one-shot call that is never
|
|
198
|
+
closed lets the seller earn the whole metered part for one response. With `closeAfter`,
|
|
199
|
+
once the paid response is in, this server does what `refund` with `close: true` does, in
|
|
200
|
+
the same call and with the same code as `metrik refund`: it signs the payer's `Close`
|
|
201
|
+
(the relay sends it and pays the gas), waits — bounded, honouring `Retry-After` — for the
|
|
202
|
+
checkpoint covering the close, and asks the relay to reclaim the unearned part to the
|
|
203
|
+
payer. It is this server acting on the model's request; the escrow does not return
|
|
204
|
+
anything by itself, and nothing is sent unless `closeAfter` is set. The wait runs after
|
|
205
|
+
the owner's spend queue is released, and can add a minute or two to the call. The result
|
|
206
|
+
adds `closeAfter` (`outcome` `completed` / `nothing-to-close` / `incomplete`, `close`,
|
|
207
|
+
`reclaim` with `amountUsdc` and `verified`, `refundedUsdc`, `incomplete.stage`,
|
|
208
|
+
`sentBy: "buyer"`, `refundIsAutomatic: false`) and `countedSpendUsdc`.
|
|
209
|
+
|
|
210
|
+
_Spend accounting:_ only what the payment finally kept counts against the mandate. A
|
|
211
|
+
reclaim whose mined receipt is verified (`ReclaimForwarded` to the payer) is recorded on
|
|
212
|
+
the stream as `reclaimedUsdc`, and cumulative spend counts `budgetUsdc - reclaimedUsdc`
|
|
213
|
+
for it (persisted by the file registry, shown by `list_streams`). A relay-reported amount
|
|
214
|
+
that nothing verified is reported but never credited back. If the close or the reclaim
|
|
215
|
+
does not finish, the tool still succeeds (the payment and the response stand), counts the
|
|
216
|
+
full price, and the text says which `refund` call finishes it. An upfront-only payment has
|
|
217
|
+
nothing to close: `closeAfter.outcome` is `nothing-to-close` and no relay is contacted.
|
|
218
|
+
|
|
196
219
|
### `refund_status` / `refund`
|
|
197
220
|
|
|
198
221
|
Arguments: `streamId`, optional `refundUrl` (not needed for a stream this server
|
package/docs/x402.md
CHANGED
|
@@ -652,8 +652,9 @@ chain and before anything is signed — and the caps are also passed to the SDK
|
|
|
652
652
|
(`X402MandateDeniedError` reason `not-an-opener-quote`).
|
|
653
653
|
|
|
654
654
|
For an upfront quote the mandate counts the WHOLE price, upfront part included,
|
|
655
|
-
as spend (`budgetUsdc = price`); that part is non-recoverable and
|
|
656
|
-
|
|
655
|
+
as spend (`budgetUsdc = price`); that part is non-recoverable, and a refund is
|
|
656
|
+
credited back only in the one case below (an MCP `closeAfter` reclaim proven by
|
|
657
|
+
its receipt). Its size is also capped separately before
|
|
657
658
|
signing by `maxUpfrontAmount` (`metrik pay --max-upfront`, MCP
|
|
658
659
|
`maxUpfrontAmountUsdc`; default 0.10 USDC, `0` refuses any upfront charge). The
|
|
659
660
|
split is shown before signing and in the result. An upfront-only payment
|
|
@@ -665,13 +666,52 @@ non-stream charge that still counts against the mandate.
|
|
|
665
666
|
```bash
|
|
666
667
|
metrik pay https://x402.137.23.50.249.sslip.io/transform --json '{"text":"# hi"}' --max-amount 0.1
|
|
667
668
|
metrik refund <streamId> --refund-url <url printed above> --close --reclaim
|
|
669
|
+
|
|
670
|
+
# one-shot call: close and reclaim in the same run
|
|
671
|
+
metrik pay https://x402.137.23.50.249.sslip.io/transform --json '{"text":"# hi"}' \
|
|
672
|
+
--max-amount 0.1 --max-upfront 0.02 --close-after
|
|
668
673
|
```
|
|
669
674
|
|
|
670
675
|
The MCP server offers the same as `x402_pay`, `refund_status` and `refund`
|
|
671
676
|
(see [mcp-reference.md](./mcp-reference.md)); `x402_pay` shares the server
|
|
672
677
|
mandate and cumulative spend with `hire_verified_service`. Refunds are still
|
|
673
678
|
**requested, never pushed**: `metrik refund` / `refund` call the relay; nothing
|
|
674
|
-
calls it for you.
|
|
679
|
+
calls it for you unless you ask for it.
|
|
680
|
+
|
|
681
|
+
### Closing right after the response (`--close-after`, `closeAfter`)
|
|
682
|
+
|
|
683
|
+
The metered part of a payment pays per verified second of the seller's uptime,
|
|
684
|
+
not per call. A one-shot caller that never closes lets the seller earn the whole
|
|
685
|
+
metered part for a single response (testers measured never closing at 4x the
|
|
686
|
+
cost of closing quickly); a caller that closes immediately gets back everything
|
|
687
|
+
unearned. So the buyer can ask for the close up front:
|
|
688
|
+
|
|
689
|
+
- `metrik pay … --close-after`
|
|
690
|
+
- MCP `x402_pay` with `closeAfter: true`
|
|
691
|
+
- `payX402WithMandate({ …, closeAfter: true | { verifyWith, maxAttempts, maxDelayMs } })`,
|
|
692
|
+
or `closeAfterPayment(…)` on an existing payment (`@absol-labs/agent/x402-buyer`)
|
|
693
|
+
|
|
694
|
+
After a successful paid response, the buyer's own client runs exactly what
|
|
695
|
+
`metrik refund --close --reclaim` runs (one shared implementation): the payer
|
|
696
|
+
signs the opener's `Close`, the seller's refund relay sends it and pays the gas,
|
|
697
|
+
the client waits — bounded, honouring the relay's `Retry-After` — for the
|
|
698
|
+
checkpoint covering the close, then asks the relay for the permissionless
|
|
699
|
+
reclaim, which the opener always pays to the payer. This is not an automatic
|
|
700
|
+
refund: it is the buyer's client sending the buyer's close and reclaim, because
|
|
701
|
+
the buyer asked; without the option nothing is sent. The payer still broadcasts
|
|
702
|
+
nothing and needs no gas.
|
|
703
|
+
|
|
704
|
+
What came back is reported from the verified receipt (`ReclaimForwarded` to the
|
|
705
|
+
payer): the refund, what the seller earned, and what the payment finally cost
|
|
706
|
+
(`price - refund` = the upfront part plus the earned seconds). An upfront-only
|
|
707
|
+
payment has nothing to close (exit 0 / `nothing-to-close`); a request that never
|
|
708
|
+
settled closes nothing. If the close or the reclaim does not finish, the payment
|
|
709
|
+
and the response stand and nothing is lost — the unearned part stays escrowed for
|
|
710
|
+
the payer: `metrik pay` exits `5` and prints the exact `metrik refund …` command
|
|
711
|
+
that finishes it; the MCP tool still succeeds and names the `refund` call; the
|
|
712
|
+
programmatic outcome is `incomplete` with the `stage`. In MCP, a verified reclaim
|
|
713
|
+
is recorded as the stream's `reclaimedUsdc`, so only what the payment kept counts
|
|
714
|
+
against the spend mandate.
|
|
675
715
|
|
|
676
716
|
The facilitator's `PAYMENT-RESPONSE` `extra.metrikStreamOpener.expiresAt` is a
|
|
677
717
|
JSON **number** (it was a decimal string before metrik-agent#121, which the SDK's
|
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.4",
|
|
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",
|
|
@@ -68,42 +68,53 @@ export const claimCommand: CommandSpec = {
|
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
|
|
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;
|
|
78
|
-
|
|
79
71
|
const write = deps.createWriteClient(
|
|
80
72
|
config,
|
|
81
73
|
account,
|
|
82
74
|
writeClientOptions(wallet),
|
|
83
75
|
);
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
spentSoFarUsdc: 0n,
|
|
91
|
-
nowSeconds: deps.nowSeconds(),
|
|
92
|
-
};
|
|
76
|
+
const generation = escrowGenerationFor(config.chainId, config.escrow);
|
|
77
|
+
// The oracle can post (or the escrow retain) a newer checkpoint between
|
|
78
|
+
// this plan and the transaction, so a proof read moments ago can revert
|
|
79
|
+
// `InvalidProof`. Nothing was spent on a reverted simulation: re-plan from
|
|
80
|
+
// fresh chain and proof state and try again, a bounded number of times.
|
|
81
|
+
let plan: Extract<ClaimPlan, { kind: "claimWithProof" | "claimProven" }>;
|
|
93
82
|
let result: { readonly txHash: `0x${string}` };
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
83
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
84
|
+
const planned = await planClaim(read, streamId, stream, generation);
|
|
85
|
+
if (planned.kind === "nothing") throw planned.error;
|
|
86
|
+
plan = planned;
|
|
87
|
+
const action = {
|
|
88
|
+
streamId,
|
|
89
|
+
// `null` for a policy-enforced wallet — see `../spend-policy.ts`.
|
|
90
|
+
signedMandate: isPolicyEnforced(wallet)
|
|
91
|
+
? null
|
|
92
|
+
: await signClaimMandate(account, config.chainId),
|
|
93
|
+
spentSoFarUsdc: 0n,
|
|
94
|
+
nowSeconds: deps.nowSeconds(),
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
if (plan.kind === "claimWithProof") {
|
|
98
|
+
result = await write.claimStream(action);
|
|
99
|
+
} else {
|
|
100
|
+
if (write.claimProvenStream === undefined) {
|
|
101
|
+
throw new CliError(
|
|
102
|
+
"this write client cannot send claimProven, which is the only way to collect this stream's proven value",
|
|
103
|
+
{
|
|
104
|
+
exitCode: 3,
|
|
105
|
+
hint: "no current checkpoint proof pays it, so a proof claim would revert NothingToClaim",
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
result = await write.claimProvenStream(action);
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (attempt >= CLAIM_REPLAN_ATTEMPTS || !isStaleProofError(error)) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
await deps.sleep(CLAIM_REPLAN_DELAY_MS);
|
|
105
117
|
}
|
|
106
|
-
result = await write.claimProvenStream(action);
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
out.result({
|
|
@@ -131,6 +142,21 @@ export const claimCommand: CommandSpec = {
|
|
|
131
142
|
},
|
|
132
143
|
};
|
|
133
144
|
|
|
145
|
+
/** How many plan-then-send rounds `metrik claim` makes before giving up. */
|
|
146
|
+
const CLAIM_REPLAN_ATTEMPTS = 3;
|
|
147
|
+
/** Pause between rounds, so a just-posted checkpoint can settle on the RPC. */
|
|
148
|
+
const CLAIM_REPLAN_DELAY_MS = 3_000;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A claim reverted because its proof no longer matches a root the escrow
|
|
152
|
+
* accepts (a checkpoint rotated, or a lagging node served a not-yet-visible
|
|
153
|
+
* root). Re-planning reads a fresh proof, so it is worth one more round.
|
|
154
|
+
*/
|
|
155
|
+
export function isStaleProofError(error: unknown): boolean {
|
|
156
|
+
const text = describeError(error);
|
|
157
|
+
return /InvalidProof|fresh checkpoint proof/.test(text);
|
|
158
|
+
}
|
|
159
|
+
|
|
134
160
|
/**
|
|
135
161
|
* Which claim, if any, `metrik claim` would send. Reads only, so it is
|
|
136
162
|
* testable (and checkable against a live stream) without a key.
|