Skip to content

Commit 90a2d4f

Browse files
authored
refactor(x402)!: drop flat back-compat fields from _meta.x402 (#1458)
## Why Closes #892. `_meta.x402` on paid tools currently carries the same payment data twice: the spec-shaped `accepts[]` array and a flat copy of the server-preferred entry (`scheme`, `network`, `amount`, ...). The flat copy was a shim for clients from before #876 that don't iterate `accepts[]`. Two shapes invite drift, and the flat one can't express more than one scheme. Consumer check before removing it: - `apify/mcpc` (v0.7.0, main a8c34fc): `selectAcceptFromToolMeta` walks `accepts[]` first and uses the flat fields only when `accepts[]` is absent; if that fails too it defers to the 402 response. PR apify/mcpc#238 merged 2026-05-26. - `apify/agentic-payments-e2e-tester` x402 canary: reads `accepts[]` first, flat shape is a legacy fallback. - Third-party clients: none on file, not verifiable from here. ## What changed Before: `_meta.x402 = { paymentRequired, ...preferredEntry, accepts }`. Now: `_meta.x402 = { paymentRequired, accepts }`. `selectPreferredAcceptEntry` and `X402_PREFERRED_SCHEMES` are deleted with it. `accepts[]`, the `structuredContent` in 402 results and `upto` support are untouched. ## Notes for reviewers (human-written) ## Proof it works Unit: `tests/unit/tools.x402.test.ts` asserts `_meta.x402` equals exactly `{ paymentRequired: true, accepts: [...] }`. The `tests/test_kit` payments case no longer expects the flat fields. Type-check and lint clean. No end-to-end mcpc transcript yet.
1 parent 71f9d1a commit 90a2d4f

3 files changed

Lines changed: 7 additions & 62 deletions

File tree

‎src/payments/x402.ts‎

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,6 @@ const X402_TOOL_INSTRUCTIONS = [
2626
'Your MCP client must support the x402 payment protocol.',
2727
].join(' ');
2828

29-
/**
30-
* Preferred scheme order when selecting the flat fields exposed on `_meta.x402`.
31-
*
32-
* `exact` first to keep the flat-fields contract back-compatible with clients
33-
* that don't iterate `accepts[]` — they continue to sign `exact` payments as
34-
* before this PR. Clients that walk `accepts[]` (post-#876 — the current
35-
* mcpc, the canary) can opt into `upto` via their scheme preference.
36-
*/
37-
const X402_PREFERRED_SCHEMES = ['exact', 'upto'] as const;
38-
3929
/**
4030
* One entry in a 402 `accepts` array. Mirrors the public x402 v2 wire shape;
4131
* carried verbatim from the Apify API.
@@ -223,25 +213,11 @@ export class X402PaymentProvider implements PaymentProvider {
223213
return new X402PaymentProvider(requirements);
224214
}
225215

226-
/**
227-
* Picks the preferred accept entry for flat `_meta.x402` advertising.
228-
* Order follows `X402_PREFERRED_SCHEMES`; falls back to the first entry
229-
* when no preferred scheme matches.
230-
*/
231-
private selectPreferredAcceptEntry(accepts: X402PaymentAccept[]): X402PaymentAccept {
232-
for (const preferred of X402_PREFERRED_SCHEMES) {
233-
const match = accepts.find((entry) => entry.scheme === preferred);
234-
if (match) return match;
235-
}
236-
return accepts[0];
237-
}
238-
239216
decorateToolSchema(tool: ToolEntry): ToolEntry {
240217
if (!tool.paymentRequired) return tool;
241218

242219
const cloned = cloneToolEntry(tool);
243220

244-
// Flat preferred fields stay for back-compat with clients that don't iterate `accepts[]`.
245221
if (!cloned._meta) {
246222
cloned._meta = {};
247223
}
@@ -250,10 +226,8 @@ export class X402PaymentProvider implements PaymentProvider {
250226
const reqs = this.requirements ? structuredClone(this.requirements) : undefined;
251227
const acceptsRaw = reqs?.accepts;
252228
const accepts = Array.isArray(acceptsRaw) && acceptsRaw.length > 0 ? acceptsRaw : undefined;
253-
const preferred = accepts ? this.selectPreferredAcceptEntry(accepts) : undefined;
254229
metaRecord.x402 = {
255230
paymentRequired: true,
256-
...(preferred ?? {}),
257231
...(accepts && { accepts }),
258232
};
259233
}

‎tests/test_kit/cases/payments.cases.ts‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ export const paymentsCases: Case[] = [
7676
try {
7777
const toolsList = await client.listTools();
7878

79-
// Positive: paid tools advertise _meta.x402 with both shapes —
80-
// flat preferred-scheme fields (back-compat) and the full accepts[] array.
79+
// Positive: paid tools advertise _meta.x402 with the full accepts[] array.
8180
for (const toolName of paidToolNames) {
8281
const tool = toolsList.tools.find((t) => t.name === toolName);
8382
expect(tool, `Tool "${toolName}" should exist in the tools list`).toBeDefined();
@@ -86,10 +85,6 @@ export const paymentsCases: Case[] = [
8685
expect(x402, `Tool "${toolName}" should advertise _meta.x402`).toBeDefined();
8786
expect(x402?.paymentRequired, `Tool "${toolName}" x402.paymentRequired should be true`).toBe(true);
8887

89-
for (const field of ['scheme', 'network', 'asset', 'payTo', 'amount'] as const) {
90-
expect(x402?.[field], `Tool "${toolName}" should advertise x402.${field}`).toBeDefined();
91-
}
92-
9388
const accepts = x402?.accepts as Record<string, unknown>[] | undefined;
9489
expect(accepts, `Tool "${toolName}" should advertise x402.accepts[]`).toBeInstanceOf(Array);
9590
expect(

‎tests/unit/tools.x402.test.ts‎

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -155,56 +155,32 @@ describe('decorateToolSchema()', () => {
155155
expect(decorated._meta).toBeUndefined();
156156
});
157157

158-
it('selects the exact entry over upto for flat fields and exposes both in accepts[]', () => {
159-
// Back-compat: clients that read only flat fields keep signing `exact` like before #876.
158+
it('exposes every scheme in accepts[] and no flat fields', () => {
160159
const requirements: X402PaymentRequirements = { x402Version: 2, accepts: [EXACT_ACCEPT, UPTO_ACCEPT] };
161160
const decorated = new X402PaymentProvider(requirements).decorateToolSchema(makePaidTool());
162161

163162
const x402 = getX402Meta(decorated);
164-
expect(x402?.paymentRequired).toBe(true);
165-
expect(x402?.scheme).toBe('exact');
166-
expect(x402?.amount).toBe(EXACT_ACCEPT.amount);
167-
expect(x402?.accepts).toEqual([EXACT_ACCEPT, UPTO_ACCEPT]);
163+
expect(x402).toEqual({ paymentRequired: true, accepts: [EXACT_ACCEPT, UPTO_ACCEPT] });
168164
});
169165

170-
it('falls back to upto when exact is not present', () => {
171-
const requirements: X402PaymentRequirements = { x402Version: 2, accepts: [UPTO_ACCEPT] };
172-
const decorated = new X402PaymentProvider(requirements).decorateToolSchema(makePaidTool());
173-
174-
const x402 = getX402Meta(decorated);
175-
expect(x402?.scheme).toBe('upto');
176-
expect(x402?.accepts).toEqual([UPTO_ACCEPT]);
177-
});
178-
179-
it('falls back to the first entry when neither exact nor upto is present', () => {
180-
const customAccept = { ...EXACT_ACCEPT, scheme: 'custom-scheme' };
181-
const requirements: X402PaymentRequirements = { x402Version: 2, accepts: [customAccept] };
182-
const decorated = new X402PaymentProvider(requirements).decorateToolSchema(makePaidTool());
183-
184-
const x402 = getX402Meta(decorated);
185-
expect(x402?.scheme).toBe('custom-scheme');
186-
expect(x402?.accepts).toEqual([customAccept]);
187-
});
188-
189-
it('preserves the configured order in accepts[] regardless of preference selection', () => {
166+
it('preserves the configured order in accepts[]', () => {
190167
// Server-emitted order may be non-deterministic upstream; whatever we receive is
191-
// what we forward. Preference only drives the flat-field selection.
168+
// what we forward.
192169
const requirements: X402PaymentRequirements = { x402Version: 2, accepts: [UPTO_ACCEPT, EXACT_ACCEPT] };
193170
const decorated = new X402PaymentProvider(requirements).decorateToolSchema(makePaidTool());
194171

195172
const x402 = getX402Meta(decorated);
196173
expect(x402?.accepts).toEqual([UPTO_ACCEPT, EXACT_ACCEPT]);
197-
expect(x402?.scheme).toBe('exact');
198174
});
199175

200-
it('marks paymentRequired without flat fields or accepts[] when requirements were not fetched', () => {
176+
it('marks paymentRequired without accepts[] when requirements were not fetched', () => {
201177
const decorated = new X402PaymentProvider(undefined).decorateToolSchema(makePaidTool());
202178

203179
const x402 = getX402Meta(decorated);
204180
expect(x402).toEqual({ paymentRequired: true });
205181
});
206182

207-
it('marks paymentRequired without flat fields or accepts[] when accepts is empty', () => {
183+
it('marks paymentRequired without accepts[] when accepts is empty', () => {
208184
const requirements: X402PaymentRequirements = { x402Version: 2, accepts: [] };
209185
const decorated = new X402PaymentProvider(requirements).decorateToolSchema(makePaidTool());
210186

0 commit comments

Comments
 (0)