@nevermined-io/payments 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2a/agent-card.d.ts +26 -0
- package/dist/a2a/agent-card.d.ts.map +1 -1
- package/dist/a2a/agent-card.js +36 -1
- package/dist/a2a/agent-card.js.map +1 -1
- package/dist/a2a/paymentsClient.d.ts +41 -1
- package/dist/a2a/paymentsClient.d.ts.map +1 -1
- package/dist/a2a/paymentsClient.js +120 -8
- package/dist/a2a/paymentsClient.js.map +1 -1
- package/dist/a2a/paymentsRequestHandler.d.ts +25 -2
- package/dist/a2a/paymentsRequestHandler.d.ts.map +1 -1
- package/dist/a2a/paymentsRequestHandler.js +240 -20
- package/dist/a2a/paymentsRequestHandler.js.map +1 -1
- package/dist/a2a/server.d.ts +2 -2
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +70 -20
- package/dist/a2a/server.js.map +1 -1
- package/dist/a2a/types.d.ts +31 -1
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/a2a/x402-a2a.d.ts +142 -0
- package/dist/a2a/x402-a2a.d.ts.map +1 -0
- package/dist/a2a/x402-a2a.js +254 -0
- package/dist/a2a/x402-a2a.js.map +1 -0
- package/dist/api/agents-api.d.ts +19 -0
- package/dist/api/agents-api.d.ts.map +1 -1
- package/dist/api/agents-api.js +30 -3
- package/dist/api/agents-api.js.map +1 -1
- package/dist/api/base-payments.d.ts +6 -4
- package/dist/api/base-payments.d.ts.map +1 -1
- package/dist/api/base-payments.js +10 -0
- package/dist/api/base-payments.js.map +1 -1
- package/dist/api/contracts-api.js +1 -1
- package/dist/api/contracts-api.js.map +1 -1
- package/dist/api/nvm-api.d.ts +1 -0
- package/dist/api/nvm-api.d.ts.map +1 -1
- package/dist/api/nvm-api.js +4 -0
- package/dist/api/nvm-api.js.map +1 -1
- package/dist/api/plans-api.d.ts +12 -2
- package/dist/api/plans-api.d.ts.map +1 -1
- package/dist/api/plans-api.js +12 -12
- package/dist/api/plans-api.js.map +1 -1
- package/dist/common/api-version.d.ts +24 -0
- package/dist/common/api-version.d.ts.map +1 -0
- package/dist/common/api-version.js +24 -0
- package/dist/common/api-version.js.map +1 -0
- package/dist/common/types.d.ts +73 -18
- package/dist/common/types.d.ts.map +1 -1
- package/dist/common/types.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/core/auth.d.ts +14 -0
- package/dist/mcp/core/auth.d.ts.map +1 -1
- package/dist/mcp/core/auth.js +56 -23
- package/dist/mcp/core/auth.js.map +1 -1
- package/dist/mcp/core/paywall.d.ts +6 -0
- package/dist/mcp/core/paywall.d.ts.map +1 -1
- package/dist/mcp/core/paywall.js +170 -84
- package/dist/mcp/core/paywall.js.map +1 -1
- package/dist/mcp/utils/errors.d.ts +26 -0
- package/dist/mcp/utils/errors.d.ts.map +1 -1
- package/dist/mcp/utils/errors.js +32 -0
- package/dist/mcp/utils/errors.js.map +1 -1
- package/dist/mcp/utils/meta.d.ts +54 -0
- package/dist/mcp/utils/meta.d.ts.map +1 -0
- package/dist/mcp/utils/meta.js +72 -0
- package/dist/mcp/utils/meta.js.map +1 -0
- package/dist/payments.d.ts +4 -3
- package/dist/payments.d.ts.map +1 -1
- package/dist/payments.js +5 -3
- package/dist/payments.js.map +1 -1
- package/dist/utils.d.ts +27 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +34 -0
- package/dist/utils.js.map +1 -1
- package/dist/x402/facilitator-api.d.ts +21 -0
- package/dist/x402/facilitator-api.d.ts.map +1 -1
- package/dist/x402/facilitator-api.js +39 -0
- package/dist/x402/facilitator-api.js.map +1 -1
- package/dist/x402/index.d.ts +1 -1
- package/dist/x402/index.d.ts.map +1 -1
- package/dist/x402/index.js.map +1 -1
- package/dist/x402/token.d.ts +13 -10
- package/dist/x402/token.d.ts.map +1 -1
- package/dist/x402/token.js +46 -16
- package/dist/x402/token.js.map +1 -1
- package/package.json +2 -2
|
@@ -3,7 +3,8 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
3
3
|
import { PaymentsError } from '../common/payments.error.js';
|
|
4
4
|
import { isValidScheme } from '../common/types.js';
|
|
5
5
|
import { decodeAccessToken } from '../utils.js';
|
|
6
|
-
import { buildPaymentRequired } from '../x402/facilitator-api.js';
|
|
6
|
+
import { buildPaymentRequired, } from '../x402/facilitator-api.js';
|
|
7
|
+
import { x402A2AUtils } from './x402-a2a.js';
|
|
7
8
|
const terminalStates = ['completed', 'failed', 'canceled', 'rejected'];
|
|
8
9
|
/**
|
|
9
10
|
* PaymentsRequestHandler extends DefaultRequestHandler to add payments validation and burning.
|
|
@@ -109,7 +110,9 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
109
110
|
throw PaymentsError.unauthorized('Cannot determine subscriberAddress from token (expected payload.authorization.from)');
|
|
110
111
|
}
|
|
111
112
|
const agentId = paymentExtension?.params?.agentId;
|
|
112
|
-
const scheme = isValidScheme(decodedAccessToken.accepted?.scheme)
|
|
113
|
+
const scheme = isValidScheme(decodedAccessToken.accepted?.scheme)
|
|
114
|
+
? decodedAccessToken.accepted.scheme
|
|
115
|
+
: 'nvm:erc4337';
|
|
113
116
|
const paymentRequired = buildPaymentRequired(planId, {
|
|
114
117
|
endpoint: endpoint || '',
|
|
115
118
|
agentId,
|
|
@@ -172,7 +175,9 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
172
175
|
if (!planId) {
|
|
173
176
|
throw PaymentsError.unauthorized('Plan ID not found in agent card.');
|
|
174
177
|
}
|
|
175
|
-
const scheme = isValidScheme(decodedAccessToken.accepted?.scheme)
|
|
178
|
+
const scheme = isValidScheme(decodedAccessToken.accepted?.scheme)
|
|
179
|
+
? decodedAccessToken.accepted.scheme
|
|
180
|
+
: 'nvm:erc4337';
|
|
176
181
|
// Build paymentRequired using the helper
|
|
177
182
|
const paymentRequired = buildPaymentRequired(planId, {
|
|
178
183
|
endpoint: httpContext?.urlRequested,
|
|
@@ -217,11 +222,17 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
217
222
|
if (!httpContext) {
|
|
218
223
|
throw A2AError.internalError('HTTP context not found for task or message.');
|
|
219
224
|
}
|
|
220
|
-
// 2. Extract bearer token and validate presence of required fields
|
|
225
|
+
// 2. Extract bearer token and validate presence of required fields.
|
|
226
|
+
// This path only runs for an *authorized* context (a token was supplied), so
|
|
227
|
+
// both bearerToken and validation must be present — assert the invariant
|
|
228
|
+
// explicitly instead of carrying an `undefined as any`.
|
|
221
229
|
const { bearerToken, validation } = httpContext;
|
|
222
230
|
if (!bearerToken) {
|
|
223
231
|
throw PaymentsError.unauthorized('Missing bearer token for payment validation.');
|
|
224
232
|
}
|
|
233
|
+
if (!validation) {
|
|
234
|
+
throw PaymentsError.unauthorized('Missing validation context for payment.');
|
|
235
|
+
}
|
|
225
236
|
// 3. Validate credits before executing the task
|
|
226
237
|
const agentCard = await this.getAgentCard();
|
|
227
238
|
const agentId = agentCard.capabilities?.extensions?.find((ext) => ext.uri === 'urn:nevermined:payment')?.params?.agentId;
|
|
@@ -271,6 +282,85 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
271
282
|
resultManager,
|
|
272
283
|
};
|
|
273
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* x402 v2 A2A transport: if the request is payment-gated and arrived with no
|
|
287
|
+
* token (the middleware stored a `paymentRequired` on the HTTP context), build
|
|
288
|
+
* and return the spec-shaped `input-required` task carrying the
|
|
289
|
+
* X402PaymentRequired object under `x402.payment.required` — WITHOUT executing
|
|
290
|
+
* the agent. Returns `undefined` when a token was present (normal flow).
|
|
291
|
+
*
|
|
292
|
+
* @param params - The incoming message send parameters
|
|
293
|
+
* @returns The `input-required` Task to return to the client, or `undefined`
|
|
294
|
+
*/
|
|
295
|
+
async buildPaymentRequiredTaskIfNeeded(params) {
|
|
296
|
+
const message = params.message;
|
|
297
|
+
if (!message) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
const incomingTaskId = message.taskId;
|
|
301
|
+
const httpContext = incomingTaskId
|
|
302
|
+
? this.getHttpRequestContextForTask(incomingTaskId)
|
|
303
|
+
: this.getHttpRequestContextForMessage(message.messageId);
|
|
304
|
+
if (!httpContext?.paymentRequired) {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
// Correlate the input-required task with the incoming taskId when present so
|
|
308
|
+
// the client's follow-up payment payload (same taskId) maps back to it.
|
|
309
|
+
const taskId = incomingTaskId || uuidv4();
|
|
310
|
+
const contextId = message.contextId || uuidv4();
|
|
311
|
+
const task = {
|
|
312
|
+
kind: 'task',
|
|
313
|
+
id: taskId,
|
|
314
|
+
contextId,
|
|
315
|
+
status: { state: 'submitted', timestamp: new Date().toISOString() },
|
|
316
|
+
history: [message],
|
|
317
|
+
};
|
|
318
|
+
const paymentRequiredTask = x402A2AUtils.createPaymentRequiredTask(task, httpContext.paymentRequired);
|
|
319
|
+
// Persist the input-required task so the client's follow-up (same taskId)
|
|
320
|
+
// can correlate to it (otherwise the SDK's _createRequestContext raises
|
|
321
|
+
// "Task not found"). The follow-up carries the in-band payload, so the
|
|
322
|
+
// middleware overwrites this taskId's HTTP context with the authorized one.
|
|
323
|
+
await this.getTaskStore().save(paymentRequiredTask);
|
|
324
|
+
if (!incomingTaskId) {
|
|
325
|
+
this.deleteHttpRequestContextForMessage(message.messageId);
|
|
326
|
+
}
|
|
327
|
+
return paymentRequiredTask;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Stamp x402 settlement state onto the final task's metadata, in band, per the
|
|
331
|
+
* x402 v2 A2A transport. Only applied when the token arrived in band (so the
|
|
332
|
+
* legacy `payment-signature` header path is unchanged). On success the task
|
|
333
|
+
* carries `x402.payment.status: payment-completed` + `x402.payment.receipts`;
|
|
334
|
+
* on failure `payment-failed` + `x402.payment.error` + receipts.
|
|
335
|
+
*
|
|
336
|
+
* @param task - The current task (mutated in place)
|
|
337
|
+
* @param httpContext - The request's HTTP context
|
|
338
|
+
* @param settlement - The settlement result, or undefined when none ran
|
|
339
|
+
*/
|
|
340
|
+
recordInBandSettlement(task, httpContext, settlement, settlementDeferred = false) {
|
|
341
|
+
if (!task || !httpContext?.inBand) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (settlement && settlement.success === false) {
|
|
345
|
+
x402A2AUtils.recordPaymentFailure(task, settlement.errorReason || 'SETTLEMENT_FAILED', settlement);
|
|
346
|
+
}
|
|
347
|
+
else if (settlement) {
|
|
348
|
+
x402A2AUtils.recordPaymentSuccess(task, settlement);
|
|
349
|
+
}
|
|
350
|
+
else if (settlementDeferred) {
|
|
351
|
+
// No in-band settlement result because redemption is BATCHED: the payload was
|
|
352
|
+
// verified but on-chain settlement is deferred out-of-band (this handler never
|
|
353
|
+
// confirms it). Mark payment-verified + the deferred marker, NOT
|
|
354
|
+
// payment-completed — so the client knows it will be charged out-of-band
|
|
355
|
+
// rather than reading a completed task as "nothing owed".
|
|
356
|
+
x402A2AUtils.recordPaymentDeferred(task);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// Defensive default: verified, but no settlement result AND not batch-deferred
|
|
360
|
+
// (e.g. a settle that returned nothing). Record a bare verify.
|
|
361
|
+
x402A2AUtils.recordPaymentVerified(task);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
274
364
|
/**
|
|
275
365
|
* Processes streaming events with finalization (credits burning and push notifications).
|
|
276
366
|
* Similar to processEventsWithFinalization but yields events for streaming.
|
|
@@ -281,7 +371,7 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
281
371
|
* @param validation - The validation result
|
|
282
372
|
* @returns Async generator yielding processed events
|
|
283
373
|
*/
|
|
284
|
-
async *processStreamingEventsWithFinalization(taskId, resultManager, eventQueue, bearerToken) {
|
|
374
|
+
async *processStreamingEventsWithFinalization(taskId, resultManager, eventQueue, bearerToken, requestHttpContext) {
|
|
285
375
|
try {
|
|
286
376
|
for await (const event of eventQueue.events()) {
|
|
287
377
|
await resultManager.processEvent(event);
|
|
@@ -298,21 +388,75 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
298
388
|
// Get redemption configuration from server (not from client metadata)
|
|
299
389
|
const redemptionConfig = await this.getRedemptionConfig();
|
|
300
390
|
if (!redemptionConfig.useBatch) {
|
|
301
|
-
//
|
|
302
|
-
|
|
391
|
+
// Prefer the per-request context (authoritative in-band flag); fall
|
|
392
|
+
// back to a per-taskId lookup for executors that mint their own task id.
|
|
393
|
+
const httpContext = requestHttpContext ?? this.getHttpRequestContextForTask(event.taskId);
|
|
303
394
|
// Execute redemption with server configuration for non-batch requests
|
|
304
395
|
const response = await this.executeRedemption(bearerToken, BigInt(event.metadata.creditsUsed), httpContext);
|
|
305
396
|
// Update event metadata with response data
|
|
306
397
|
if (response && event.metadata) {
|
|
307
|
-
event.metadata.txHash = response.txHash;
|
|
398
|
+
event.metadata.txHash = response.txHash ?? response.transaction;
|
|
308
399
|
event.metadata.creditsCharged = response.amountOfCredits
|
|
309
400
|
? Number(response.amountOfCredits)
|
|
310
401
|
: event.metadata.creditsUsed;
|
|
311
402
|
}
|
|
403
|
+
// x402 v2 A2A transport: stamp the settlement receipt onto the
|
|
404
|
+
// persisted task in band. NOTE: a stream cannot retract the event
|
|
405
|
+
// it already yielded above, so an in-band settlement is reflected
|
|
406
|
+
// on the saved task (visible via tasks/get + resubscribe) — it
|
|
407
|
+
// cannot withhold content the way a non-streaming result does.
|
|
408
|
+
if (httpContext?.inBand) {
|
|
409
|
+
const task = resultManager.getCurrentTask();
|
|
410
|
+
if (task) {
|
|
411
|
+
this.recordInBandSettlement(task, httpContext, response);
|
|
412
|
+
await resultManager.processEvent(task);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
// Batch redemption: on-chain settlement is deferred out-of-band. The
|
|
418
|
+
// stream already yielded the content (can't retract), but stamp the
|
|
419
|
+
// PERSISTED task with payment-verified + the deferred marker so a
|
|
420
|
+
// streaming client (via tasks/get + resubscribe) isn't left reading a
|
|
421
|
+
// completed task as "nothing owed" — matching the non-streaming path.
|
|
422
|
+
const httpContext = requestHttpContext ?? this.getHttpRequestContextForTask(event.taskId);
|
|
423
|
+
if (httpContext?.inBand) {
|
|
424
|
+
const task = resultManager.getCurrentTask();
|
|
425
|
+
if (task) {
|
|
426
|
+
this.recordInBandSettlement(task, httpContext, undefined, true);
|
|
427
|
+
await resultManager.processEvent(task);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
312
430
|
}
|
|
313
431
|
}
|
|
314
432
|
catch (err) {
|
|
315
|
-
//
|
|
433
|
+
// x402 v2 A2A: settlement failed AFTER the paid event was already
|
|
434
|
+
// streamed to the client (a stream cannot retract it). Do NOT swallow
|
|
435
|
+
// it — log, and stamp payment-failed on the persisted task so
|
|
436
|
+
// tasks/get + resubscribe reflect the failure (mirrors non-streaming).
|
|
437
|
+
console.error('[PaymentsA2A] streaming settlement failed after execution:', err);
|
|
438
|
+
const httpContext = requestHttpContext ?? this.getHttpRequestContextForTask(event.taskId);
|
|
439
|
+
if (httpContext?.inBand) {
|
|
440
|
+
try {
|
|
441
|
+
const task = resultManager.getCurrentTask();
|
|
442
|
+
if (task) {
|
|
443
|
+
const errorReason = err instanceof Error ? err.message : String(err);
|
|
444
|
+
if (task.status)
|
|
445
|
+
task.status.state = 'failed';
|
|
446
|
+
task.artifacts = undefined;
|
|
447
|
+
this.recordInBandSettlement(task, httpContext, {
|
|
448
|
+
success: false,
|
|
449
|
+
errorReason,
|
|
450
|
+
transaction: '',
|
|
451
|
+
network: '',
|
|
452
|
+
});
|
|
453
|
+
await resultManager.processEvent(task);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch (stampErr) {
|
|
457
|
+
console.error('[PaymentsA2A] failed to stamp streaming payment-failed:', stampErr);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
316
460
|
}
|
|
317
461
|
}
|
|
318
462
|
// Handle push notification
|
|
@@ -345,7 +489,7 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
345
489
|
* Processes all events, calling handleTaskFinalization when a terminal status-update event is received.
|
|
346
490
|
* In async mode, it can be launched in background.
|
|
347
491
|
*/
|
|
348
|
-
async processEventsWithFinalization(taskId, resultManager, eventQueue, bearerToken, validation, options) {
|
|
492
|
+
async processEventsWithFinalization(taskId, resultManager, eventQueue, bearerToken, validation, options, requestHttpContext) {
|
|
349
493
|
let firstResultSent = false;
|
|
350
494
|
try {
|
|
351
495
|
for await (const event of eventQueue.events()) {
|
|
@@ -353,8 +497,11 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
353
497
|
if (event.kind === 'status-update' &&
|
|
354
498
|
event.final &&
|
|
355
499
|
terminalStates.includes(event.status?.state)) {
|
|
356
|
-
//
|
|
357
|
-
|
|
500
|
+
// Prefer the per-request HTTP context (authoritative for this request,
|
|
501
|
+
// carries the in-band flag). Fall back to a per-taskId lookup for
|
|
502
|
+
// executors that mint their own task id (the event's taskId may then
|
|
503
|
+
// differ from the request's generated one).
|
|
504
|
+
const httpContext = requestHttpContext ?? this.getHttpRequestContextForTask(event.taskId);
|
|
358
505
|
await this.handleTaskFinalization(resultManager, event, bearerToken, httpContext);
|
|
359
506
|
}
|
|
360
507
|
await resultManager.processEvent(event);
|
|
@@ -387,8 +534,14 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
387
534
|
* @returns The resulting message or task
|
|
388
535
|
*/
|
|
389
536
|
async sendMessage(params) {
|
|
537
|
+
// x402 v2 A2A transport: a payment-gated request with no token returns an
|
|
538
|
+
// `input-required` task (in band) instead of executing the agent.
|
|
539
|
+
const paymentRequiredTask = await this.buildPaymentRequiredTaskIfNeeded(params);
|
|
540
|
+
if (paymentRequiredTask) {
|
|
541
|
+
return paymentRequiredTask;
|
|
542
|
+
}
|
|
390
543
|
// Create PaymentsRequestContext and related data
|
|
391
|
-
const { paymentsRequestContext, taskId, bearerToken, validation, requestContext, finalMessageForAgent, eventBus, eventQueue, resultManager, } = await this.createPaymentsRequestContext(params, false);
|
|
544
|
+
const { paymentsRequestContext, taskId, httpContext, bearerToken, validation, requestContext, finalMessageForAgent, eventBus, eventQueue, resultManager, } = await this.createPaymentsRequestContext(params, false);
|
|
392
545
|
this.agentExecutor
|
|
393
546
|
.execute(paymentsRequestContext, eventBus)
|
|
394
547
|
.catch((err) => {
|
|
@@ -429,7 +582,7 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
429
582
|
// The blocking parameter comes from params.configuration.blocking
|
|
430
583
|
const isBlocking = params.configuration?.blocking !== false; // Default to blocking if not specified
|
|
431
584
|
if (isBlocking) {
|
|
432
|
-
await this.processEventsWithFinalization(taskId, resultManager, eventQueue, bearerToken, validation);
|
|
585
|
+
await this.processEventsWithFinalization(taskId, resultManager, eventQueue, bearerToken, validation, undefined, httpContext);
|
|
433
586
|
const finalResult = resultManager.getFinalResult();
|
|
434
587
|
if (!finalResult) {
|
|
435
588
|
throw A2AError.internalError('Agent execution finished without a result, and no task context found.');
|
|
@@ -446,7 +599,7 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
446
599
|
this.processEventsWithFinalization(validTaskId, resultManager, eventQueue, bearerToken, validation, {
|
|
447
600
|
firstResultResolver: resolve,
|
|
448
601
|
firstResultRejector: reject,
|
|
449
|
-
});
|
|
602
|
+
}, httpContext);
|
|
450
603
|
});
|
|
451
604
|
}
|
|
452
605
|
}
|
|
@@ -467,37 +620,97 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
467
620
|
(typeof creditsToBurn === 'string' ||
|
|
468
621
|
typeof creditsToBurn === 'number' ||
|
|
469
622
|
typeof creditsToBurn === 'bigint')) {
|
|
623
|
+
let settlement;
|
|
624
|
+
let settlementError;
|
|
625
|
+
let settlementDeferred = false;
|
|
470
626
|
try {
|
|
471
627
|
// Get redemption configuration from server (not from client metadata)
|
|
472
628
|
const redemptionConfig = await this.getRedemptionConfig();
|
|
629
|
+
// Batch redemption defers on-chain settlement out-of-band (no receipt here).
|
|
630
|
+
settlementDeferred = redemptionConfig.useBatch ?? false;
|
|
473
631
|
if (!redemptionConfig.useBatch) {
|
|
474
632
|
// Execute redemption with server configuration for non-batch requests
|
|
475
633
|
const response = await this.executeRedemption(bearerToken, BigInt(creditsToBurn), httpContext);
|
|
634
|
+
settlement = response;
|
|
476
635
|
// Update event metadata with redemption results
|
|
477
636
|
event.metadata = {
|
|
478
637
|
...event.metadata,
|
|
479
|
-
txHash: response.txHash,
|
|
638
|
+
txHash: response.txHash ?? response.transaction,
|
|
480
639
|
// Store the actual credits charged (especially important for margin-based)
|
|
481
640
|
creditsCharged: response.amountOfCredits
|
|
482
641
|
? Number(response.amountOfCredits)
|
|
483
642
|
: creditsToBurn,
|
|
484
643
|
};
|
|
485
644
|
}
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
// Settlement failed AFTER the agent executed. Always log it (the legacy
|
|
648
|
+
// header path has no other surface for it). For the in-band x402 v2 A2A
|
|
649
|
+
// path it is additionally surfaced below as payment-failed + suppressed
|
|
650
|
+
// content; the legacy header path still delivers the result (no retract).
|
|
651
|
+
console.error('[PaymentsA2A] settlement failed after execution:', err);
|
|
652
|
+
settlementError = err;
|
|
653
|
+
}
|
|
654
|
+
// Stamp the in-band x402 state onto the FINAL event so it survives when the
|
|
655
|
+
// caller processes this event into the task's status (mutating the task
|
|
656
|
+
// here would be overwritten by the final event). The event carries a Task
|
|
657
|
+
// shape under `event.status`; the helpers mutate `event.status.message`.
|
|
658
|
+
if (httpContext?.inBand && settlementError) {
|
|
659
|
+
// x402 v2 A2A transport: a settlement failure after execution must NOT
|
|
660
|
+
// deliver paid content — replace the agent's status message with a
|
|
661
|
+
// failed one carrying payment-failed metadata + an error receipt.
|
|
662
|
+
const errorReason = settlementError instanceof Error ? settlementError.message : String(settlementError);
|
|
663
|
+
event.status = {
|
|
664
|
+
state: 'failed',
|
|
665
|
+
message: {
|
|
666
|
+
kind: 'message',
|
|
667
|
+
messageId: uuidv4(),
|
|
668
|
+
role: 'agent',
|
|
669
|
+
parts: [{ kind: 'text', text: `Payment settlement failed: ${errorReason}` }],
|
|
670
|
+
taskId: event.taskId,
|
|
671
|
+
contextId: event.contextId,
|
|
672
|
+
metadata: {},
|
|
673
|
+
},
|
|
674
|
+
timestamp: new Date().toISOString(),
|
|
675
|
+
};
|
|
676
|
+
this.recordInBandSettlement({ status: event.status }, httpContext, {
|
|
677
|
+
success: false,
|
|
678
|
+
errorReason,
|
|
679
|
+
transaction: '',
|
|
680
|
+
network: settlement?.network || '',
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
else if (httpContext?.inBand) {
|
|
684
|
+
// Stamp in-band settlement state onto the final event's status message.
|
|
685
|
+
this.recordInBandSettlement({ status: event.status }, httpContext, settlement, settlementDeferred);
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
486
688
|
// Always update task metadata and process the task
|
|
487
689
|
const task = resultManager.getCurrentTask();
|
|
488
690
|
if (task) {
|
|
489
|
-
// Update task metadata with current event metadata (
|
|
691
|
+
// Update task metadata with current event metadata (executor / redemption).
|
|
490
692
|
task.metadata = {
|
|
491
693
|
...task.metadata,
|
|
492
694
|
...event.metadata,
|
|
493
695
|
};
|
|
696
|
+
if (httpContext?.inBand && settlementError) {
|
|
697
|
+
// x402 v2 A2A transport: a settlement failure must never deliver paid
|
|
698
|
+
// content. The agent's status message was already replaced above; also
|
|
699
|
+
// drop any artifacts it emitted so the paid result cannot surface there
|
|
700
|
+
// (mirrors the Python SDK's _apply_inband_settlement). History is rebuilt
|
|
701
|
+
// by processEvent below from the replaced (failed) status, so it carries
|
|
702
|
+
// no paid content.
|
|
703
|
+
task.artifacts = undefined;
|
|
704
|
+
}
|
|
494
705
|
await resultManager.processEvent(task);
|
|
495
706
|
// Delete http context associated with the task
|
|
496
707
|
this.deleteHttpRequestContextForTask(event.taskId);
|
|
497
708
|
}
|
|
498
709
|
}
|
|
499
710
|
catch (err) {
|
|
500
|
-
//
|
|
711
|
+
// This block persists the payment-failed/suppressed task; if it throws the
|
|
712
|
+
// suppression itself failed, so log loudly rather than swallow.
|
|
713
|
+
console.error('[PaymentsA2A] failed to persist finalized task:', err);
|
|
501
714
|
}
|
|
502
715
|
}
|
|
503
716
|
try {
|
|
@@ -519,8 +732,15 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
519
732
|
* @returns Async generator of events
|
|
520
733
|
*/
|
|
521
734
|
async *sendMessageStream(params) {
|
|
735
|
+
// x402 v2 A2A transport: a payment-gated request with no token yields a
|
|
736
|
+
// single `input-required` task (in band) instead of executing the agent.
|
|
737
|
+
const paymentRequiredTask = await this.buildPaymentRequiredTaskIfNeeded(params);
|
|
738
|
+
if (paymentRequiredTask) {
|
|
739
|
+
yield paymentRequiredTask;
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
522
742
|
// Create PaymentsRequestContext and related data
|
|
523
|
-
const { paymentsRequestContext, taskId, bearerToken, requestContext, finalMessageForAgent, eventBus, eventQueue, resultManager, } = await this.createPaymentsRequestContext(params, true);
|
|
743
|
+
const { paymentsRequestContext, taskId, httpContext, bearerToken, requestContext, finalMessageForAgent, eventBus, eventQueue, resultManager, } = await this.createPaymentsRequestContext(params, true);
|
|
524
744
|
this.agentExecutor
|
|
525
745
|
.execute(paymentsRequestContext, eventBus)
|
|
526
746
|
.catch((err) => {
|
|
@@ -547,7 +767,7 @@ export class PaymentsRequestHandler extends DefaultRequestHandler {
|
|
|
547
767
|
eventBus.publish(errorTaskStatus);
|
|
548
768
|
});
|
|
549
769
|
// Process streaming events with finalization
|
|
550
|
-
yield* this.processStreamingEventsWithFinalization(taskId, resultManager, eventQueue, bearerToken);
|
|
770
|
+
yield* this.processStreamingEventsWithFinalization(taskId, resultManager, eventQueue, bearerToken, httpContext);
|
|
551
771
|
}
|
|
552
772
|
/**
|
|
553
773
|
* Sends a push notification when a task reaches a terminal state.
|