@nevermined-io/payments 1.7.0 → 1.9.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.
Files changed (88) hide show
  1. package/README.md +2 -1
  2. package/dist/a2a/agent-card.d.ts +26 -0
  3. package/dist/a2a/agent-card.d.ts.map +1 -1
  4. package/dist/a2a/agent-card.js +36 -1
  5. package/dist/a2a/agent-card.js.map +1 -1
  6. package/dist/a2a/paymentsClient.d.ts +41 -1
  7. package/dist/a2a/paymentsClient.d.ts.map +1 -1
  8. package/dist/a2a/paymentsClient.js +120 -8
  9. package/dist/a2a/paymentsClient.js.map +1 -1
  10. package/dist/a2a/paymentsRequestHandler.d.ts +25 -2
  11. package/dist/a2a/paymentsRequestHandler.d.ts.map +1 -1
  12. package/dist/a2a/paymentsRequestHandler.js +240 -20
  13. package/dist/a2a/paymentsRequestHandler.js.map +1 -1
  14. package/dist/a2a/server.d.ts +2 -2
  15. package/dist/a2a/server.d.ts.map +1 -1
  16. package/dist/a2a/server.js +70 -20
  17. package/dist/a2a/server.js.map +1 -1
  18. package/dist/a2a/types.d.ts +31 -1
  19. package/dist/a2a/types.d.ts.map +1 -1
  20. package/dist/a2a/types.js.map +1 -1
  21. package/dist/a2a/x402-a2a.d.ts +142 -0
  22. package/dist/a2a/x402-a2a.d.ts.map +1 -0
  23. package/dist/a2a/x402-a2a.js +254 -0
  24. package/dist/a2a/x402-a2a.js.map +1 -0
  25. package/dist/api/agents-api.d.ts +19 -0
  26. package/dist/api/agents-api.d.ts.map +1 -1
  27. package/dist/api/agents-api.js +28 -1
  28. package/dist/api/agents-api.js.map +1 -1
  29. package/dist/api/nvm-api.d.ts +1 -0
  30. package/dist/api/nvm-api.d.ts.map +1 -1
  31. package/dist/api/nvm-api.js +4 -0
  32. package/dist/api/nvm-api.js.map +1 -1
  33. package/dist/api/plans-api.d.ts +12 -2
  34. package/dist/api/plans-api.d.ts.map +1 -1
  35. package/dist/api/plans-api.js +9 -2
  36. package/dist/api/plans-api.js.map +1 -1
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +3 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/mcp/core/auth.d.ts +16 -2
  42. package/dist/mcp/core/auth.d.ts.map +1 -1
  43. package/dist/mcp/core/auth.js +70 -26
  44. package/dist/mcp/core/auth.js.map +1 -1
  45. package/dist/mcp/core/paywall.d.ts +6 -0
  46. package/dist/mcp/core/paywall.d.ts.map +1 -1
  47. package/dist/mcp/core/paywall.js +179 -89
  48. package/dist/mcp/core/paywall.js.map +1 -1
  49. package/dist/mcp/core/server-manager.d.ts.map +1 -1
  50. package/dist/mcp/core/server-manager.js +6 -4
  51. package/dist/mcp/core/server-manager.js.map +1 -1
  52. package/dist/mcp/http/client-registration.d.ts.map +1 -1
  53. package/dist/mcp/http/client-registration.js +4 -2
  54. package/dist/mcp/http/client-registration.js.map +1 -1
  55. package/dist/mcp/http/oauth-metadata.d.ts.map +1 -1
  56. package/dist/mcp/http/oauth-metadata.js +2 -1
  57. package/dist/mcp/http/oauth-metadata.js.map +1 -1
  58. package/dist/mcp/index.d.ts +12 -5
  59. package/dist/mcp/index.d.ts.map +1 -1
  60. package/dist/mcp/index.js +46 -23
  61. package/dist/mcp/index.js.map +1 -1
  62. package/dist/mcp/types/http.types.d.ts +2 -2
  63. package/dist/mcp/types/http.types.d.ts.map +1 -1
  64. package/dist/mcp/types/http.types.js.map +1 -1
  65. package/dist/mcp/types/paywall.types.d.ts +6 -2
  66. package/dist/mcp/types/paywall.types.d.ts.map +1 -1
  67. package/dist/mcp/types/paywall.types.js.map +1 -1
  68. package/dist/mcp/types/server.types.d.ts +4 -2
  69. package/dist/mcp/types/server.types.d.ts.map +1 -1
  70. package/dist/mcp/types/server.types.js.map +1 -1
  71. package/dist/mcp/utils/errors.d.ts +26 -0
  72. package/dist/mcp/utils/errors.d.ts.map +1 -1
  73. package/dist/mcp/utils/errors.js +32 -0
  74. package/dist/mcp/utils/errors.js.map +1 -1
  75. package/dist/mcp/utils/meta.d.ts +54 -0
  76. package/dist/mcp/utils/meta.d.ts.map +1 -0
  77. package/dist/mcp/utils/meta.js +72 -0
  78. package/dist/mcp/utils/meta.js.map +1 -0
  79. package/dist/payments.d.ts.map +1 -1
  80. package/dist/utils.d.ts +27 -0
  81. package/dist/utils.d.ts.map +1 -1
  82. package/dist/utils.js +34 -0
  83. package/dist/utils.js.map +1 -1
  84. package/dist/x402/facilitator-api.d.ts +21 -0
  85. package/dist/x402/facilitator-api.d.ts.map +1 -1
  86. package/dist/x402/facilitator-api.js +39 -0
  87. package/dist/x402/facilitator-api.js.map +1 -1
  88. package/package.json +2 -2
@@ -1,10 +1,10 @@
1
1
  import { decodeAccessToken } from '../../utils.js';
2
2
  import { getCurrentRequestContext } from '../http/mcp-handler.js';
3
- import { ERROR_CODES, createRpcError } from '../utils/errors.js';
3
+ import { PaymentRequiredError } from '../utils/errors.js';
4
4
  import { isValidScheme } from '../../common/types.js';
5
5
  import { buildLogicalMetaUrl, buildLogicalUrl } from '../utils/logical-url.js';
6
6
  import { extractAuthHeader, stripBearer } from '../utils/request.js';
7
- import { buildPaymentRequired } from '../../x402/facilitator-api.js';
7
+ import { buildPaymentRequired, buildPaymentRequiredForPlans, } from '../../x402/facilitator-api.js';
8
8
  /**
9
9
  * Handles authentication and authorization for MCP requests
10
10
  */
@@ -95,24 +95,67 @@ export class PaywallAuthenticator {
95
95
  // HTTP fallback also failed
96
96
  }
97
97
  }
98
- // Both attempts failed — enrich denial with suggested plans (best-effort)
99
- let plansMsg = '';
100
- try {
101
- const plans = await this.payments.agents.getAgentPlans(agentId);
102
- if (plans && Array.isArray(plans.plans) && plans.plans.length > 0) {
103
- const top = plans.plans.slice(0, 3);
104
- const summary = top
105
- .map((p) => `${p.planId || p.id || 'plan'}${p.name ? ` (${p.name})` : ''}`)
106
- .join(', ');
107
- plansMsg = summary ? ` Available plans: ${summary}...` : '';
98
+ // Both attempts failed — surface a spec-shaped PaymentRequired error
99
+ // (converted in-band to a tool-result error for tools; propagates as a
100
+ // JSON-RPC error for resources/prompts).
101
+ throw await this.buildPaymentRequiredError(agentId, logicalUrl, 'Payment required.', planIdOverride);
102
+ }
103
+ /**
104
+ * Build a spec-shaped {@link PaymentRequiredError} from the agent's plans.
105
+ *
106
+ * Fetches the agent's plans (best-effort) to populate the `accepts` array of
107
+ * the `PaymentRequired` object and a human-readable list of plan names in the
108
+ * error message. Falls back to an empty plan id when no plans can be resolved
109
+ * so the structured shape is still valid.
110
+ *
111
+ * @param agentId - Agent identifier used to look up purchasable plans.
112
+ * @param endpoint - Logical resource URL placed in `PaymentRequired.resource`.
113
+ * @param message - Leading human-readable message (e.g. "Authorization required.").
114
+ * @returns A `PaymentRequiredError` carrying the `PaymentRequired` object.
115
+ */
116
+ async buildPaymentRequiredError(agentId, endpoint, message = 'Payment required.', fallbackPlanId) {
117
+ const planIds = [];
118
+ const names = [];
119
+ let plansLookupFailed = false;
120
+ // Only look up the agent's plans when an agentId is configured. Under the
121
+ // plan-centric model agentId is optional, so we advertise the configured
122
+ // plan directly (below) instead of requiring an agent lookup.
123
+ if (agentId) {
124
+ try {
125
+ const plans = await this.payments.agents.getAgentPlans(agentId);
126
+ if (plans && Array.isArray(plans.plans)) {
127
+ for (const p of plans.plans) {
128
+ const pid = p.planId || p.id;
129
+ if (pid)
130
+ planIds.push(pid);
131
+ if (pid)
132
+ names.push(`${pid}${p.name ? ` (${p.name})` : ''}`);
133
+ }
134
+ }
135
+ }
136
+ catch (error) {
137
+ // Best-effort: a backend failure must not look like a clean "unpaid".
138
+ plansLookupFailed = true;
139
+ console.error(`[x402] Failed to fetch agent plans while building payment-required (agentId=${agentId}): ${error instanceof Error ? error.message : String(error)}`);
108
140
  }
109
141
  }
110
- catch {
111
- // Ignore errors fetching plans - best effort only
142
+ // Plan-centric fallback: advertise the configured plan when no plans were
143
+ // resolved via the agent (or no agentId was provided).
144
+ if (planIds.length === 0 && fallbackPlanId) {
145
+ planIds.push(fallbackPlanId);
112
146
  }
113
- throw createRpcError(ERROR_CODES.PaymentRequired, `Payment required.${plansMsg}`, {
114
- reason: 'invalid',
147
+ const plansMsg = names.length > 0 ? ` Available plans: ${names.slice(0, 3).join(', ')}...` : '';
148
+ const paymentRequired = buildPaymentRequiredForPlans(planIds, {
149
+ endpoint,
150
+ agentId,
151
+ httpVerb: 'POST',
152
+ environment: this.payments.getEnvironmentName(),
115
153
  });
154
+ // When the plans lookup itself failed (backend outage) the `accepts` array
155
+ // falls back to an empty plan id; flag it so a client can't mistake the
156
+ // resulting payment-required for a clean "free / no plan needed" response.
157
+ paymentRequired.error = plansLookupFailed ? 'plans unavailable' : 'payment required';
158
+ return new PaymentRequiredError(paymentRequired, `${message}${plansMsg}`);
116
159
  }
117
160
  /**
118
161
  * Verify permissions against a single endpoint URL.
@@ -126,7 +169,8 @@ export class PaywallAuthenticator {
126
169
  let planId = planIdOverride ?? decodedAccessToken.accepted?.planId;
127
170
  const subscriberAddress = decodedAccessToken.payload?.authorization?.from;
128
171
  // If planId is not available, try to get it from the agent's plans
129
- if (!planId) {
172
+ // (only possible when an agentId is configured).
173
+ if (!planId && agentId) {
130
174
  try {
131
175
  const agentPlans = await this.payments.agents.getAgentPlans(agentId);
132
176
  if (agentPlans && Array.isArray(agentPlans.plans) && agentPlans.plans.length > 0) {
@@ -140,7 +184,9 @@ export class PaywallAuthenticator {
140
184
  if (!planId || !subscriberAddress) {
141
185
  throw new Error('Cannot determine plan_id or subscriber_address from token (expected accepted.planId and payload.authorization.from)');
142
186
  }
143
- const scheme = isValidScheme(decodedAccessToken?.accepted?.scheme) ? decodedAccessToken.accepted.scheme : 'nvm:erc4337';
187
+ const scheme = isValidScheme(decodedAccessToken?.accepted?.scheme)
188
+ ? decodedAccessToken.accepted.scheme
189
+ : 'nvm:erc4337';
144
190
  const paymentRequired = buildPaymentRequired(planId, {
145
191
  endpoint,
146
192
  agentId,
@@ -162,15 +208,14 @@ export class PaywallAuthenticator {
162
208
  * Authenticate an MCP request
163
209
  */
164
210
  async authenticate(extra, options = {}, agentId, serverName, name, kind, argsOrVars) {
211
+ const logicalUrl = buildLogicalUrl({ kind, serverName, name, argsOrVars });
165
212
  const authHeader = this.extractAuthHeaderFromContext(extra);
166
213
  if (!authHeader) {
167
- throw createRpcError(ERROR_CODES.PaymentRequired, 'Authorization required', {
168
- reason: 'missing',
169
- });
214
+ throw await this.buildPaymentRequiredError(agentId, logicalUrl, 'Authorization required.', options.planId);
170
215
  }
171
216
  return this.verifyWithFallback({
172
217
  accessToken: stripBearer(authHeader),
173
- logicalUrl: buildLogicalUrl({ kind, serverName, name, argsOrVars }),
218
+ logicalUrl,
174
219
  httpUrl: this.buildHttpUrlFromContext(),
175
220
  maxAmount: options.maxAmount ?? 1n,
176
221
  agentId,
@@ -182,15 +227,14 @@ export class PaywallAuthenticator {
182
227
  * Returns an AuthResult compatible with paywall flows (without redeem step).
183
228
  */
184
229
  async authenticateMeta(extra, options = {}, agentId, serverName, method) {
230
+ const logicalUrl = buildLogicalMetaUrl(serverName, method);
185
231
  const authHeader = this.extractAuthHeaderFromContext(extra);
186
232
  if (!authHeader) {
187
- throw createRpcError(ERROR_CODES.PaymentRequired, 'Authorization required', {
188
- reason: 'missing',
189
- });
233
+ throw await this.buildPaymentRequiredError(agentId, logicalUrl, 'Authorization required.', options.planId);
190
234
  }
191
235
  return this.verifyWithFallback({
192
236
  accessToken: stripBearer(authHeader),
193
- logicalUrl: buildLogicalMetaUrl(serverName, method),
237
+ logicalUrl,
194
238
  httpUrl: this.buildHttpUrlFromContext(),
195
239
  maxAmount: options.maxAmount ?? 1n,
196
240
  agentId,
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/mcp/core/auth.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authentication handler for MCP paywall using X402 tokens\n */\nimport type { Payments } from '../../payments.js'\nimport { decodeAccessToken } from '../../utils.js'\nimport { getCurrentRequestContext } from '../http/mcp-handler.js'\nimport { AuthResult } from '../types/paywall.types.js'\nimport { ERROR_CODES, createRpcError } from '../utils/errors.js'\nimport { Address, isValidScheme } from '../../common/types.js'\nimport { buildLogicalMetaUrl, buildLogicalUrl } from '../utils/logical-url.js'\nimport { extractAuthHeader, stripBearer } from '../utils/request.js'\nimport { buildPaymentRequired, type X402PaymentRequired } from '../../x402/facilitator-api.js'\n\ninterface VerifyContext {\n accessToken: string\n logicalUrl: string\n httpUrl: string | undefined\n maxAmount: bigint\n agentId: string\n planIdOverride?: string\n}\n\n/**\n * Handles authentication and authorization for MCP requests\n */\nexport class PaywallAuthenticator {\n constructor(private payments: Payments) {}\n\n /**\n * Extract authorization header from extra context or AsyncLocalStorage.\n * Tries SDK's extra context first, then falls back to HTTP request context.\n *\n * @param extra - MCP extra context from SDK\n * @returns Authorization header value or undefined\n */\n private extractAuthHeaderFromContext(extra: any): string | undefined {\n // Try to extract auth header from SDK's extra context first\n let authHeader = extractAuthHeader(extra)\n\n if (!authHeader) {\n const requestContext = getCurrentRequestContext()\n if (requestContext?.headers) {\n // Build an extra-like object for extractAuthHeader\n authHeader = extractAuthHeader({ requestInfo: { headers: requestContext.headers } })\n }\n }\n\n return authHeader\n }\n\n /**\n * Build HTTP endpoint URL from request context.\n *\n * @returns HTTP endpoint URL or undefined if context is not available\n */\n private buildHttpUrlFromContext(): string | undefined {\n const requestContext = getCurrentRequestContext()\n if (!requestContext) {\n return undefined\n }\n\n try {\n const host = requestContext.headers?.['host'] || requestContext.headers?.['x-forwarded-host']\n if (!host || typeof host !== 'string') {\n return undefined\n }\n\n const protocol = requestContext.headers?.['x-forwarded-proto'] || 'http'\n const baseUrl = `${protocol}://${host}`\n\n // Use requestContext.url if available (e.g., '/mcp'), otherwise default to '/mcp'\n const path = requestContext.url || '/mcp'\n return `${baseUrl}${path}`\n } catch {\n return undefined\n }\n }\n\n /**\n * Core verification logic shared by authenticate and authenticateMeta.\n * Tries logical URL first, falls back to HTTP URL if available.\n */\n private async verifyWithFallback(ctx: VerifyContext): Promise<AuthResult> {\n const { accessToken, logicalUrl, httpUrl, maxAmount, agentId, planIdOverride } = ctx\n\n // Try logical URL first\n try {\n const result = await this.verifyWithEndpoint(\n accessToken,\n logicalUrl,\n agentId,\n maxAmount,\n planIdOverride,\n )\n return {\n token: accessToken,\n agentId,\n logicalUrl,\n httpUrl,\n planId: result.planId,\n subscriberAddress: result.subscriberAddress,\n agentRequest: result.agentRequest,\n }\n } catch {\n // If logical URL fails and we have an HTTP URL, try that\n }\n\n if (httpUrl) {\n try {\n const result = await this.verifyWithEndpoint(\n accessToken,\n httpUrl,\n agentId,\n maxAmount,\n planIdOverride,\n )\n return {\n token: accessToken,\n agentId,\n logicalUrl,\n httpUrl,\n planId: result.planId,\n subscriberAddress: result.subscriberAddress,\n agentRequest: result.agentRequest,\n }\n } catch {\n // HTTP fallback also failed\n }\n }\n\n // Both attempts failed — enrich denial with suggested plans (best-effort)\n let plansMsg = ''\n try {\n const plans = await this.payments.agents.getAgentPlans(agentId)\n if (plans && Array.isArray(plans.plans) && plans.plans.length > 0) {\n const top = plans.plans.slice(0, 3)\n const summary = top\n .map((p: any) => `${p.planId || p.id || 'plan'}${p.name ? ` (${p.name})` : ''}`)\n .join(', ')\n plansMsg = summary ? ` Available plans: ${summary}...` : ''\n }\n } catch {\n // Ignore errors fetching plans - best effort only\n }\n\n throw createRpcError(ERROR_CODES.PaymentRequired, `Payment required.${plansMsg}`, {\n reason: 'invalid',\n })\n }\n\n /**\n * Verify permissions against a single endpoint URL.\n * Resolves planId from the token or from the agent's plans as fallback.\n */\n private async verifyWithEndpoint(\n accessToken: string,\n endpoint: string,\n agentId: string,\n maxAmount: bigint,\n planIdOverride?: string,\n ): Promise<{ planId: string; subscriberAddress: Address; agentRequest?: any }> {\n const decodedAccessToken = decodeAccessToken(accessToken)\n if (!decodedAccessToken) {\n throw new Error('Invalid access token')\n }\n\n let planId = planIdOverride ?? decodedAccessToken.accepted?.planId\n const subscriberAddress = decodedAccessToken.payload?.authorization?.from\n\n // If planId is not available, try to get it from the agent's plans\n if (!planId) {\n try {\n const agentPlans = await this.payments.agents.getAgentPlans(agentId)\n if (agentPlans && Array.isArray(agentPlans.plans) && agentPlans.plans.length > 0) {\n planId = agentPlans.plans[0].planId || agentPlans.plans[0].id\n }\n } catch {\n // Ignore errors fetching plans\n }\n }\n\n if (!planId || !subscriberAddress) {\n throw new Error(\n 'Cannot determine plan_id or subscriber_address from token (expected accepted.planId and payload.authorization.from)',\n )\n }\n\n const scheme = isValidScheme(decodedAccessToken?.accepted?.scheme) ? decodedAccessToken.accepted.scheme : 'nvm:erc4337'\n const paymentRequired: X402PaymentRequired = buildPaymentRequired(planId, {\n endpoint,\n agentId,\n httpVerb: 'POST',\n scheme,\n environment: this.payments.getEnvironmentName(),\n })\n\n const result = await this.payments.facilitator.verifyPermissions({\n paymentRequired,\n x402AccessToken: accessToken,\n maxAmount,\n })\n\n if (!result.isValid) {\n throw new Error('Permission verification failed')\n }\n\n return { planId, subscriberAddress, agentRequest: result.agentRequest }\n }\n\n /**\n * Authenticate an MCP request\n */\n async authenticate(\n extra: any,\n options: { planId?: string; maxAmount?: bigint } = {},\n agentId: string,\n serverName: string,\n name: string,\n kind: 'tool' | 'resource' | 'prompt',\n argsOrVars: any,\n ): Promise<AuthResult> {\n const authHeader = this.extractAuthHeaderFromContext(extra)\n if (!authHeader) {\n throw createRpcError(ERROR_CODES.PaymentRequired, 'Authorization required', {\n reason: 'missing',\n })\n }\n\n return this.verifyWithFallback({\n accessToken: stripBearer(authHeader),\n logicalUrl: buildLogicalUrl({ kind, serverName, name, argsOrVars }),\n httpUrl: this.buildHttpUrlFromContext(),\n maxAmount: options.maxAmount ?? 1n,\n agentId,\n planIdOverride: options.planId,\n })\n }\n\n /**\n * Authenticate generic MCP meta operations (e.g., initialize, tools/list, resources/list, prompts/list).\n * Returns an AuthResult compatible with paywall flows (without redeem step).\n */\n async authenticateMeta(\n extra: any,\n options: { planId?: string; maxAmount?: bigint } = {},\n agentId: string,\n serverName: string,\n method: string,\n ): Promise<AuthResult> {\n const authHeader = this.extractAuthHeaderFromContext(extra)\n if (!authHeader) {\n throw createRpcError(ERROR_CODES.PaymentRequired, 'Authorization required', {\n reason: 'missing',\n })\n }\n\n return this.verifyWithFallback({\n accessToken: stripBearer(authHeader),\n logicalUrl: buildLogicalMetaUrl(serverName, method),\n httpUrl: this.buildHttpUrlFromContext(),\n maxAmount: options.maxAmount ?? 1n,\n agentId,\n planIdOverride: options.planId,\n })\n }\n}\n"]}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/mcp/core/auth.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authentication handler for MCP paywall using X402 tokens\n */\nimport type { Payments } from '../../payments.js'\nimport { decodeAccessToken } from '../../utils.js'\nimport { getCurrentRequestContext } from '../http/mcp-handler.js'\nimport { AuthResult } from '../types/paywall.types.js'\nimport { PaymentRequiredError } from '../utils/errors.js'\nimport { Address, isValidScheme } from '../../common/types.js'\nimport { buildLogicalMetaUrl, buildLogicalUrl } from '../utils/logical-url.js'\nimport { extractAuthHeader, stripBearer } from '../utils/request.js'\nimport {\n buildPaymentRequired,\n buildPaymentRequiredForPlans,\n type X402PaymentRequired,\n} from '../../x402/facilitator-api.js'\n\ninterface VerifyContext {\n accessToken: string\n logicalUrl: string\n httpUrl: string | undefined\n maxAmount: bigint\n agentId?: string\n planIdOverride?: string\n}\n\n/**\n * Handles authentication and authorization for MCP requests\n */\nexport class PaywallAuthenticator {\n constructor(private payments: Payments) {}\n\n /**\n * Extract authorization header from extra context or AsyncLocalStorage.\n * Tries SDK's extra context first, then falls back to HTTP request context.\n *\n * @param extra - MCP extra context from SDK\n * @returns Authorization header value or undefined\n */\n private extractAuthHeaderFromContext(extra: any): string | undefined {\n // Try to extract auth header from SDK's extra context first\n let authHeader = extractAuthHeader(extra)\n\n if (!authHeader) {\n const requestContext = getCurrentRequestContext()\n if (requestContext?.headers) {\n // Build an extra-like object for extractAuthHeader\n authHeader = extractAuthHeader({ requestInfo: { headers: requestContext.headers } })\n }\n }\n\n return authHeader\n }\n\n /**\n * Build HTTP endpoint URL from request context.\n *\n * @returns HTTP endpoint URL or undefined if context is not available\n */\n private buildHttpUrlFromContext(): string | undefined {\n const requestContext = getCurrentRequestContext()\n if (!requestContext) {\n return undefined\n }\n\n try {\n const host = requestContext.headers?.['host'] || requestContext.headers?.['x-forwarded-host']\n if (!host || typeof host !== 'string') {\n return undefined\n }\n\n const protocol = requestContext.headers?.['x-forwarded-proto'] || 'http'\n const baseUrl = `${protocol}://${host}`\n\n // Use requestContext.url if available (e.g., '/mcp'), otherwise default to '/mcp'\n const path = requestContext.url || '/mcp'\n return `${baseUrl}${path}`\n } catch {\n return undefined\n }\n }\n\n /**\n * Core verification logic shared by authenticate and authenticateMeta.\n * Tries logical URL first, falls back to HTTP URL if available.\n */\n private async verifyWithFallback(ctx: VerifyContext): Promise<AuthResult> {\n const { accessToken, logicalUrl, httpUrl, maxAmount, agentId, planIdOverride } = ctx\n\n // Try logical URL first\n try {\n const result = await this.verifyWithEndpoint(\n accessToken,\n logicalUrl,\n agentId,\n maxAmount,\n planIdOverride,\n )\n return {\n token: accessToken,\n agentId,\n logicalUrl,\n httpUrl,\n planId: result.planId,\n subscriberAddress: result.subscriberAddress,\n agentRequest: result.agentRequest,\n }\n } catch {\n // If logical URL fails and we have an HTTP URL, try that\n }\n\n if (httpUrl) {\n try {\n const result = await this.verifyWithEndpoint(\n accessToken,\n httpUrl,\n agentId,\n maxAmount,\n planIdOverride,\n )\n return {\n token: accessToken,\n agentId,\n logicalUrl,\n httpUrl,\n planId: result.planId,\n subscriberAddress: result.subscriberAddress,\n agentRequest: result.agentRequest,\n }\n } catch {\n // HTTP fallback also failed\n }\n }\n\n // Both attempts failed — surface a spec-shaped PaymentRequired error\n // (converted in-band to a tool-result error for tools; propagates as a\n // JSON-RPC error for resources/prompts).\n throw await this.buildPaymentRequiredError(agentId, logicalUrl, 'Payment required.', planIdOverride)\n }\n\n /**\n * Build a spec-shaped {@link PaymentRequiredError} from the agent's plans.\n *\n * Fetches the agent's plans (best-effort) to populate the `accepts` array of\n * the `PaymentRequired` object and a human-readable list of plan names in the\n * error message. Falls back to an empty plan id when no plans can be resolved\n * so the structured shape is still valid.\n *\n * @param agentId - Agent identifier used to look up purchasable plans.\n * @param endpoint - Logical resource URL placed in `PaymentRequired.resource`.\n * @param message - Leading human-readable message (e.g. \"Authorization required.\").\n * @returns A `PaymentRequiredError` carrying the `PaymentRequired` object.\n */\n private async buildPaymentRequiredError(\n agentId: string | undefined,\n endpoint: string,\n message = 'Payment required.',\n fallbackPlanId?: string,\n ): Promise<PaymentRequiredError> {\n const planIds: string[] = []\n const names: string[] = []\n let plansLookupFailed = false\n // Only look up the agent's plans when an agentId is configured. Under the\n // plan-centric model agentId is optional, so we advertise the configured\n // plan directly (below) instead of requiring an agent lookup.\n if (agentId) {\n try {\n const plans = await this.payments.agents.getAgentPlans(agentId)\n if (plans && Array.isArray(plans.plans)) {\n for (const p of plans.plans) {\n const pid = p.planId || p.id\n if (pid) planIds.push(pid)\n if (pid) names.push(`${pid}${p.name ? ` (${p.name})` : ''}`)\n }\n }\n } catch (error) {\n // Best-effort: a backend failure must not look like a clean \"unpaid\".\n plansLookupFailed = true\n console.error(\n `[x402] Failed to fetch agent plans while building payment-required (agentId=${agentId}): ${\n error instanceof Error ? error.message : String(error)\n }`,\n )\n }\n }\n\n // Plan-centric fallback: advertise the configured plan when no plans were\n // resolved via the agent (or no agentId was provided).\n if (planIds.length === 0 && fallbackPlanId) {\n planIds.push(fallbackPlanId)\n }\n\n const plansMsg = names.length > 0 ? ` Available plans: ${names.slice(0, 3).join(', ')}...` : ''\n\n const paymentRequired = buildPaymentRequiredForPlans(planIds, {\n endpoint,\n agentId,\n httpVerb: 'POST',\n environment: this.payments.getEnvironmentName(),\n }) as X402PaymentRequired & { error?: string }\n // When the plans lookup itself failed (backend outage) the `accepts` array\n // falls back to an empty plan id; flag it so a client can't mistake the\n // resulting payment-required for a clean \"free / no plan needed\" response.\n paymentRequired.error = plansLookupFailed ? 'plans unavailable' : 'payment required'\n\n return new PaymentRequiredError(paymentRequired, `${message}${plansMsg}`)\n }\n\n /**\n * Verify permissions against a single endpoint URL.\n * Resolves planId from the token or from the agent's plans as fallback.\n */\n private async verifyWithEndpoint(\n accessToken: string,\n endpoint: string,\n agentId: string | undefined,\n maxAmount: bigint,\n planIdOverride?: string,\n ): Promise<{ planId: string; subscriberAddress: Address; agentRequest?: any }> {\n const decodedAccessToken = decodeAccessToken(accessToken)\n if (!decodedAccessToken) {\n throw new Error('Invalid access token')\n }\n\n let planId = planIdOverride ?? decodedAccessToken.accepted?.planId\n const subscriberAddress = decodedAccessToken.payload?.authorization?.from\n\n // If planId is not available, try to get it from the agent's plans\n // (only possible when an agentId is configured).\n if (!planId && agentId) {\n try {\n const agentPlans = await this.payments.agents.getAgentPlans(agentId)\n if (agentPlans && Array.isArray(agentPlans.plans) && agentPlans.plans.length > 0) {\n planId = agentPlans.plans[0].planId || agentPlans.plans[0].id\n }\n } catch {\n // Ignore errors fetching plans\n }\n }\n\n if (!planId || !subscriberAddress) {\n throw new Error(\n 'Cannot determine plan_id or subscriber_address from token (expected accepted.planId and payload.authorization.from)',\n )\n }\n\n const scheme = isValidScheme(decodedAccessToken?.accepted?.scheme)\n ? decodedAccessToken.accepted.scheme\n : 'nvm:erc4337'\n const paymentRequired: X402PaymentRequired = buildPaymentRequired(planId, {\n endpoint,\n agentId,\n httpVerb: 'POST',\n scheme,\n environment: this.payments.getEnvironmentName(),\n })\n\n const result = await this.payments.facilitator.verifyPermissions({\n paymentRequired,\n x402AccessToken: accessToken,\n maxAmount,\n })\n\n if (!result.isValid) {\n throw new Error('Permission verification failed')\n }\n\n return { planId, subscriberAddress, agentRequest: result.agentRequest }\n }\n\n /**\n * Authenticate an MCP request\n */\n async authenticate(\n extra: any,\n options: { planId?: string; maxAmount?: bigint } = {},\n agentId: string | undefined,\n serverName: string,\n name: string,\n kind: 'tool' | 'resource' | 'prompt',\n argsOrVars: any,\n ): Promise<AuthResult> {\n const logicalUrl = buildLogicalUrl({ kind, serverName, name, argsOrVars })\n\n const authHeader = this.extractAuthHeaderFromContext(extra)\n if (!authHeader) {\n throw await this.buildPaymentRequiredError(\n agentId,\n logicalUrl,\n 'Authorization required.',\n options.planId,\n )\n }\n\n return this.verifyWithFallback({\n accessToken: stripBearer(authHeader),\n logicalUrl,\n httpUrl: this.buildHttpUrlFromContext(),\n maxAmount: options.maxAmount ?? 1n,\n agentId,\n planIdOverride: options.planId,\n })\n }\n\n /**\n * Authenticate generic MCP meta operations (e.g., initialize, tools/list, resources/list, prompts/list).\n * Returns an AuthResult compatible with paywall flows (without redeem step).\n */\n async authenticateMeta(\n extra: any,\n options: { planId?: string; maxAmount?: bigint } = {},\n agentId: string | undefined,\n serverName: string,\n method: string,\n ): Promise<AuthResult> {\n const logicalUrl = buildLogicalMetaUrl(serverName, method)\n\n const authHeader = this.extractAuthHeaderFromContext(extra)\n if (!authHeader) {\n throw await this.buildPaymentRequiredError(\n agentId,\n logicalUrl,\n 'Authorization required.',\n options.planId,\n )\n }\n\n return this.verifyWithFallback({\n accessToken: stripBearer(authHeader),\n logicalUrl,\n httpUrl: this.buildHttpUrlFromContext(),\n maxAmount: options.maxAmount ?? 1n,\n agentId,\n planIdOverride: options.planId,\n })\n }\n}\n"]}
@@ -24,6 +24,12 @@ export declare class PaywallDecorator {
24
24
  * Internal method to create the wrapped handler
25
25
  */
26
26
  private createWrappedHandler;
27
+ /**
28
+ * Build a spec-shaped `PaymentRequired` dict for a settlement failure, from
29
+ * the authenticated request context. Surfaced (with tool content suppressed)
30
+ * when settlement fails after the tool has executed.
31
+ */
32
+ private buildPaymentRequiredFromAuth;
27
33
  /**
28
34
  * Redeem credits after successful request
29
35
  */
@@ -1 +1 @@
1
- {"version":3,"file":"paywall.d.ts","sourceRoot":"","sources":["../../../src/mcp/core/paywall.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAOjD,OAAO,EACL,SAAS,EAET,aAAa,EACb,eAAe,EACf,WAAW,EACZ,MAAM,2BAA2B,CAAA;AAElC,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAE7D;;GAEG;AACH,qBAAa,gBAAgB;IAQzB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,cAAc;IARxB,OAAO,CAAC,MAAM,CAGb;gBAGS,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,oBAAoB,EACnC,cAAc,EAAE,sBAAsB;IAGhD;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI;IAOnC;;OAEG;IAEH,OAAO,CAAC,KAAK,GAAG,GAAG,EACjB,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,EACzD,OAAO,EAAE,WAAW,GAAG,aAAa,GACnC,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;IAC7C,OAAO,CACL,OAAO,EAAE,CACP,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,EAC5C,KAAK,CAAC,EAAE,GAAG,KACR,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,EACvB,OAAO,EAAE,eAAe,GACvB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;IAKxF;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAiH5B;;OAEG;YACW,aAAa;CAoE5B"}
1
+ {"version":3,"file":"paywall.d.ts","sourceRoot":"","sources":["../../../src/mcp/core/paywall.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAQjD,OAAO,EAEL,SAAS,EAET,aAAa,EACb,eAAe,EACf,WAAW,EACZ,MAAM,2BAA2B,CAAA;AAalC,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAM7D;;GAEG;AACH,qBAAa,gBAAgB;IAQzB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,cAAc;IARxB,OAAO,CAAC,MAAM,CAGb;gBAGS,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,oBAAoB,EACnC,cAAc,EAAE,sBAAsB;IAGhD;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI;IAQnC;;OAEG;IAEH,OAAO,CAAC,KAAK,GAAG,GAAG,EACjB,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,EACzD,OAAO,EAAE,WAAW,GAAG,aAAa,GACnC,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;IAC7C,OAAO,CACL,OAAO,EAAE,CACP,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,EAC5C,KAAK,CAAC,EAAE,GAAG,KACR,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,EACvB,OAAO,EAAE,eAAe,GACvB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;IAKxF;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA6K5B;;;;OAIG;IACH,OAAO,CAAC,4BAA4B;IAYpC;;OAEG;YACW,aAAa;CAyE5B"}
@@ -2,9 +2,13 @@
2
2
  * Main paywall decorator for MCP handlers (tools, resources, prompts)
3
3
  */
4
4
  import { isValidScheme } from '../../common/types.js';
5
- import { decodeAccessToken } from '../../utils.js';
6
- import { buildPaymentRequired, } from '../../x402/facilitator-api.js';
7
- import { ERROR_CODES, createRpcError } from '../utils/errors.js';
5
+ import { decodeAccessToken, encodeAccessToken } from '../../utils.js';
6
+ import { buildPaymentRequired, buildPaymentRequiredForPlans, } from '../../x402/facilitator-api.js';
7
+ import { ERROR_CODES, PaymentRequiredError, SettlementFailedError, createRpcError, } from '../utils/errors.js';
8
+ import { NEVERMINED_CREDITS_META_KEY, X402_PAYMENT_RESPONSE_META_KEY, paymentRequiredResult, readPaymentPayload, } from '../utils/meta.js';
9
+ // Emit the Authorization-header deprecation notice at most once per process to
10
+ // avoid log spam on high-traffic servers still using the legacy header path.
11
+ let authHeaderDeprecationWarned = false;
8
12
  /**
9
13
  * Main class for creating paywall-protected MCP handlers
10
14
  */
@@ -15,7 +19,7 @@ export class PaywallDecorator {
15
19
  this.creditsContext = creditsContext;
16
20
  // Internal config ensures serverName is always a concrete string
17
21
  this.config = {
18
- agentId: '',
22
+ planId: '',
19
23
  serverName: 'mcp-server',
20
24
  };
21
25
  }
@@ -24,7 +28,8 @@ export class PaywallDecorator {
24
28
  */
25
29
  configure(options) {
26
30
  this.config = {
27
- agentId: options.agentId || this.config.agentId,
31
+ planId: options.planId || this.config.planId,
32
+ agentId: options.agentId ?? this.config.agentId,
28
33
  serverName: options.serverName ?? this.config.serverName,
29
34
  };
30
35
  }
@@ -36,9 +41,12 @@ export class PaywallDecorator {
36
41
  */
37
42
  createWrappedHandler(handler, options) {
38
43
  return async (...allArgs) => {
39
- // Validate configuration
40
- if (!this.config.agentId) {
41
- throw createRpcError(ERROR_CODES.Misconfiguration, 'Server misconfiguration: missing agentId');
44
+ // Validate configuration: a planId must be resolvable (per-tool option or
45
+ // server-level config). agentId is optional under the plan-centric model
46
+ // (the facilitator resolves everything from planId + token).
47
+ const configuredPlanId = options?.planId ?? this.config.planId;
48
+ if (!configuredPlanId) {
49
+ throw createRpcError(ERROR_CODES.Misconfiguration, 'Server misconfiguration: missing planId');
42
50
  }
43
51
  const kind = options?.kind ?? 'tool';
44
52
  const name = options?.name ?? 'unnamed';
@@ -46,81 +54,150 @@ export class PaywallDecorator {
46
54
  const isResource = allArgs.length >= 2 && allArgs[0] instanceof URL;
47
55
  const extra = isResource ? allArgs[2] : allArgs[1];
48
56
  const argsOrVars = isResource ? allArgs[1] : allArgs[0];
49
- // 1. Authenticate request
50
- const authResult = await this.authenticator.authenticate(extra, { planId: options?.planId, maxAmount: options?.maxAmount }, this.config.agentId, this.config.serverName, name, kind, argsOrVars);
51
- // 2. Pre-calculate credits if they are fixed (not a function)
52
- // This allows handlers to access credits during execution
53
- const creditsOption = options?.credits;
54
- const isFixedCredits = typeof creditsOption === 'bigint' || creditsOption === undefined;
55
- const preCalculatedCredits = isFixedCredits
56
- ? this.creditsContext.resolve(creditsOption, argsOrVars, null, authResult)
57
- : undefined;
58
- // Determine effective planId: explicit option overrides token-derived value
59
- const effectivePlanId = options?.planId ?? authResult.planId;
60
- // 3. Build PaywallContext for handler (with extra wrapper for backward compatibility)
61
- const paywallContext = {
62
- authResult,
63
- credits: preCalculatedCredits,
64
- planId: authResult.planId,
65
- subscriberAddress: authResult.subscriberAddress,
66
- agentRequest: authResult.agentRequest,
67
- };
68
- // 4. Execute original handler with context
69
- const result = await handler(...allArgs, paywallContext);
70
- // 5. Resolve final credits to burn (may be different if credits are dynamic)
71
- const credits = isFixedCredits
72
- ? (preCalculatedCredits ?? 1n)
73
- : this.creditsContext.resolve(creditsOption, argsOrVars, result, authResult);
74
- // Update context with final resolved credits
75
- paywallContext.credits = credits;
76
- // 6. If the result is an AsyncIterable (stream), redeem on completion
77
- if (isAsyncIterable(result)) {
78
- const onFinally = async () => {
79
- return await this.redeemCredits(effectivePlanId, authResult.token, authResult.subscriberAddress, credits, options, authResult.agentId, authResult.logicalUrl, authResult.httpUrl, 'POST');
57
+ try {
58
+ // x402 v2 MCP transport: prefer the in-band payment payload from
59
+ // params._meta["x402/payment"]. Re-encode it into the access token
60
+ // string the verify/settle path expects and present it via the same
61
+ // extra/headers shape the auth flow reads, so the in-band payload takes
62
+ // precedence over the Authorization header (kept as a deprecated
63
+ // fallback when the in-band payload is absent). The RAW extra is still
64
+ // forwarded to the user handler below.
65
+ const paymentPayload = readPaymentPayload(extra);
66
+ let authExtra = extra;
67
+ if (paymentPayload) {
68
+ const token = encodeAccessToken(paymentPayload);
69
+ // Synthesize an auth-only extra carrying the in-band token. This
70
+ // intentionally drops the rest of `extra` for the AUTH call only; the
71
+ // RAW `extra` (with `_meta`) is still forwarded to the user handler below.
72
+ authExtra = { requestInfo: { headers: { authorization: `Bearer ${token}` } } };
73
+ }
74
+ else if (!authHeaderDeprecationWarned) {
75
+ authHeaderDeprecationWarned = true;
76
+ console.warn('[x402] No _meta["x402/payment"] on the MCP request; falling back to the ' +
77
+ 'Authorization header (deprecated under the x402 v2 MCP transport).');
78
+ }
79
+ // 1. Authenticate request
80
+ const authResult = await this.authenticator.authenticate(authExtra, { planId: configuredPlanId, maxAmount: options?.maxAmount }, this.config.agentId, this.config.serverName, name, kind, argsOrVars);
81
+ // 2. Pre-calculate credits if they are fixed (not a function)
82
+ // This allows handlers to access credits during execution
83
+ const creditsOption = options?.credits;
84
+ const isFixedCredits = typeof creditsOption === 'bigint' || creditsOption === undefined;
85
+ const preCalculatedCredits = isFixedCredits
86
+ ? this.creditsContext.resolve(creditsOption, argsOrVars, null, authResult)
87
+ : undefined;
88
+ // Determine effective planId: explicit option overrides token-derived value
89
+ const effectivePlanId = options?.planId ?? authResult.planId;
90
+ // 3. Build PaywallContext for handler (with extra wrapper for backward compatibility)
91
+ const paywallContext = {
92
+ authResult,
93
+ credits: preCalculatedCredits,
94
+ planId: authResult.planId,
95
+ subscriberAddress: authResult.subscriberAddress,
96
+ agentRequest: authResult.agentRequest,
80
97
  };
81
- return wrapAsyncIterable(result, onFinally, effectivePlanId, authResult.subscriberAddress, credits);
98
+ // 4. Execute original handler with context
99
+ const result = await handler(...allArgs, paywallContext);
100
+ // 5. Resolve final credits to burn (may be different if credits are dynamic)
101
+ const credits = isFixedCredits
102
+ ? (preCalculatedCredits ?? 1n)
103
+ : this.creditsContext.resolve(creditsOption, argsOrVars, result, authResult);
104
+ // Update context with final resolved credits
105
+ paywallContext.credits = credits;
106
+ // 6. If the result is an AsyncIterable (stream), redeem on completion
107
+ if (isAsyncIterable(result)) {
108
+ const onFinally = async () => {
109
+ return await this.redeemCredits(effectivePlanId, authResult.token, authResult.subscriberAddress, credits, options, authResult.agentId, authResult.logicalUrl, authResult.httpUrl, 'POST');
110
+ };
111
+ return wrapAsyncIterable(result, onFinally, effectivePlanId, authResult.subscriberAddress, credits);
112
+ }
113
+ // 7. Non-streaming: redeem immediately
114
+ const creditsResult = await this.redeemCredits(effectivePlanId, authResult.token, authResult.subscriberAddress, credits, options, authResult.agentId, authResult.logicalUrl,
115
+ // fix: pre-existing arg order — fallbackEndpoint=httpUrl, httpVerb='POST'
116
+ // (matches the streaming site above)
117
+ authResult.httpUrl, 'POST');
118
+ // Settlement failed AFTER the tool executed: per the x402 v2 MCP
119
+ // transport spec, do NOT return the tool's content — surface only the
120
+ // payment error so a paid result is never delivered without payment
121
+ // landing. (onRedeemError "ignore" therefore no longer delivers paid
122
+ // content; "propagate" already threw a Misconfiguration in redeemCredits.)
123
+ if (creditsResult && !creditsResult.success) {
124
+ console.error(`[x402] settlement failed after tool execution; suppressing tool content. reason=${creditsResult.errorReason}`);
125
+ throw new SettlementFailedError(this.buildPaymentRequiredFromAuth(authResult));
126
+ }
127
+ // creditsResult is undefined for free / no-credit calls (no settlement
128
+ // performed) — in that case the spec receipt is omitted. On success the
129
+ // full receipt goes under the spec key; Nevermined observability is kept
130
+ // under a namespaced key so it never collides with the spec shape.
131
+ result._meta = {
132
+ ...result._meta,
133
+ ...(creditsResult && { [X402_PAYMENT_RESPONSE_META_KEY]: creditsResult }),
134
+ [NEVERMINED_CREDITS_META_KEY]: {
135
+ ...(creditsResult?.transaction && { txHash: creditsResult.transaction }),
136
+ creditsRedeemed: creditsResult?.success
137
+ ? (creditsResult.creditsRedeemed ?? credits.toString())
138
+ : '0',
139
+ remainingBalance: creditsResult?.remainingBalance,
140
+ planId: authResult.planId,
141
+ subscriberAddress: authResult.subscriberAddress,
142
+ success: creditsResult ? creditsResult.success : true,
143
+ },
144
+ };
145
+ return result;
146
+ }
147
+ catch (error) {
148
+ // Payment-required (pre-execution, from auth) and settlement-failure
149
+ // (post-execution) are surfaced in band as an error tool result for
150
+ // tools. Resources/prompts have no tool-result error channel, so the
151
+ // error propagates as a JSON-RPC error instead.
152
+ if (error instanceof PaymentRequiredError && kind === 'tool') {
153
+ return paymentRequiredResult(error.paymentRequired);
154
+ }
155
+ throw error;
82
156
  }
83
- // 7. Non-streaming: redeem immediately
84
- const creditsResult = await this.redeemCredits(effectivePlanId, authResult.token, authResult.subscriberAddress, credits, options, authResult.agentId, authResult.logicalUrl, 'POST', authResult.httpUrl);
85
- result._meta = {
86
- ...result._meta,
87
- ...(creditsResult.transaction && { txHash: creditsResult.transaction }),
88
- creditsRedeemed: creditsResult.success ? (creditsResult.creditsRedeemed ?? credits.toString()) : '0',
89
- remainingBalance: creditsResult.remainingBalance,
90
- planId: authResult.planId,
91
- subscriberAddress: authResult.subscriberAddress,
92
- success: creditsResult.success,
93
- ...(creditsResult.errorReason && { errorReason: creditsResult.errorReason }),
94
- };
95
- return result;
96
157
  };
97
158
  }
159
+ /**
160
+ * Build a spec-shaped `PaymentRequired` dict for a settlement failure, from
161
+ * the authenticated request context. Surfaced (with tool content suppressed)
162
+ * when settlement fails after the tool has executed.
163
+ */
164
+ buildPaymentRequiredFromAuth(authResult) {
165
+ const planId = authResult.planId || '';
166
+ const paymentRequired = buildPaymentRequiredForPlans(planId ? [planId] : [''], {
167
+ endpoint: authResult.logicalUrl || authResult.httpUrl,
168
+ agentId: authResult.agentId,
169
+ httpVerb: 'POST',
170
+ environment: this.payments.getEnvironmentName(),
171
+ });
172
+ paymentRequired.error = 'settlement failed';
173
+ return paymentRequired;
174
+ }
98
175
  /**
99
176
  * Redeem credits after successful request
100
177
  */
101
178
  async redeemCredits(planId, token, subscriberAddress, credits, options, agentId, endpoint, fallbackEndpoint, httpVerb) {
102
- let ret = {
103
- success: true,
104
- transaction: '',
105
- network: '',
106
- };
179
+ // No settlement for free / no-credit calls — signalled to the caller as
180
+ // `undefined` so the spec receipt (_meta["x402/payment-response"]) is omitted.
181
+ if (!(credits && credits > 0n && subscriberAddress && planId)) {
182
+ return undefined;
183
+ }
107
184
  const decoded = decodeAccessToken(token);
108
- const scheme = isValidScheme(decoded?.accepted?.scheme) ? decoded.accepted.scheme : 'nvm:erc4337';
185
+ const scheme = isValidScheme(decoded?.accepted?.scheme)
186
+ ? decoded.accepted.scheme
187
+ : 'nvm:erc4337';
109
188
  try {
110
- if (credits && credits > 0n && subscriberAddress && planId) {
111
- const paymentRequired = buildPaymentRequired(planId, {
112
- endpoint: endpoint || '',
113
- agentId,
114
- httpVerb,
115
- scheme,
116
- environment: this.payments.getEnvironmentName(),
117
- });
118
- ret = await this.payments.facilitator.settlePermissions({
119
- paymentRequired,
120
- x402AccessToken: token,
121
- maxAmount: credits,
122
- });
123
- }
189
+ const paymentRequired = buildPaymentRequired(planId, {
190
+ endpoint: endpoint || '',
191
+ agentId,
192
+ httpVerb,
193
+ scheme,
194
+ environment: this.payments.getEnvironmentName(),
195
+ });
196
+ return await this.payments.facilitator.settlePermissions({
197
+ paymentRequired,
198
+ x402AccessToken: token,
199
+ maxAmount: credits,
200
+ });
124
201
  }
125
202
  catch (primaryError) {
126
203
  // If logical URL fails and we have an HTTP URL fallback, retry with it
@@ -134,26 +211,27 @@ export class PaywallDecorator {
134
211
  scheme,
135
212
  environment: this.payments.getEnvironmentName(),
136
213
  });
137
- ret = await this.payments.facilitator.settlePermissions({
214
+ return await this.payments.facilitator.settlePermissions({
138
215
  paymentRequired,
139
216
  x402AccessToken: token,
140
217
  maxAmount: credits,
141
218
  });
142
- return ret;
143
219
  }
144
220
  catch (fallbackError) {
145
221
  // Fallback also failed, use fallback error as the reported error
146
222
  lastError = fallbackError;
147
223
  }
148
224
  }
149
- ret.success = false;
150
- ret.errorReason = lastError instanceof Error ? lastError.message : String(lastError);
225
+ const errorReason = lastError instanceof Error ? lastError.message : String(lastError);
226
+ console.error(`[x402] settle failed: ${errorReason}`);
151
227
  if (options.onRedeemError === 'propagate') {
152
- throw createRpcError(ERROR_CODES.Misconfiguration, `Failed to redeem credits: ${ret.errorReason}`);
228
+ throw createRpcError(ERROR_CODES.Misconfiguration, `Failed to redeem credits: ${errorReason}`);
153
229
  }
154
- // Default: attach error to result but don't throw
230
+ // Default ("ignore"): return a failed result so the caller suppresses the
231
+ // tool content and surfaces the in-band payment error (always-suppress
232
+ // under the x402 v2 MCP transport).
233
+ return { success: false, transaction: '', network: '', errorReason };
155
234
  }
156
- return ret;
157
235
  }
158
236
  }
159
237
  /**
@@ -176,17 +254,29 @@ function wrapAsyncIterable(iterable, onFinally, planId, subscriberAddress, credi
176
254
  finally {
177
255
  creditsResult = await onFinally();
178
256
  }
179
- // Yield a _meta chunk at the end with the redemption result
257
+ // Yield a _meta chunk at the end with the redemption result.
258
+ // NOTE: a stream cannot retroactively suppress already-yielded chunks, so a
259
+ // post-execution settlement failure on a stream is only reported here in the
260
+ // final _meta chunk (under nevermined/credits) — it cannot withhold content
261
+ // the way a non-streaming tool result does. `creditsResult` is undefined for
262
+ // free / no-credit calls.
263
+ const settlement = creditsResult || undefined;
180
264
  const metadataChunk = {
181
265
  _meta: {
182
- // Only include txHash if it has a value
183
- ...(creditsResult?.transaction && { txHash: creditsResult.transaction }),
184
- creditsRedeemed: creditsResult?.success ? (creditsResult.creditsRedeemed ?? credits.toString()) : '0',
185
- remainingBalance: creditsResult?.remainingBalance,
186
- planId,
187
- subscriberAddress,
188
- success: creditsResult?.success || false,
189
- ...(creditsResult?.errorReason && { errorReason: creditsResult.errorReason }),
266
+ // Spec receipt only on a successful settlement.
267
+ ...(settlement?.success && { [X402_PAYMENT_RESPONSE_META_KEY]: settlement }),
268
+ // Nevermined-namespaced observability (NOT part of the x402 spec).
269
+ [NEVERMINED_CREDITS_META_KEY]: {
270
+ ...(settlement?.transaction && { txHash: settlement.transaction }),
271
+ creditsRedeemed: settlement?.success
272
+ ? (settlement.creditsRedeemed ?? credits.toString())
273
+ : '0',
274
+ remainingBalance: settlement?.remainingBalance,
275
+ planId,
276
+ subscriberAddress,
277
+ success: settlement ? settlement.success : true,
278
+ ...(settlement?.errorReason && { errorReason: settlement.errorReason }),
279
+ },
190
280
  },
191
281
  };
192
282
  yield metadataChunk;