From 96687d47fe39ef9a3ef6cc8965e2b2e569869dd8 Mon Sep 17 00:00:00 2001 From: R-M-Naveen Date: Thu, 28 May 2026 16:38:16 -0500 Subject: [PATCH] Carry resource metadata in MPP challenges --- .../atxp-server/src/omniChallenge.test.ts | 21 ++++++++++++++++++- packages/atxp-server/src/omniChallenge.ts | 21 +++++++++++++++++-- packages/atxp-server/src/protocol.ts | 4 ++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/atxp-server/src/omniChallenge.test.ts b/packages/atxp-server/src/omniChallenge.test.ts index 898e760..6f2a331 100644 --- a/packages/atxp-server/src/omniChallenge.test.ts +++ b/packages/atxp-server/src/omniChallenge.test.ts @@ -213,7 +213,11 @@ describe('omniChallenge', () => { { network: 'tempo', currency: 'pathUSD', address: '0xTempo', amount: new BigNumber('0.01') }, ]; - const result = buildMppChallenge({ id: 'ch_123', options }); + const result = buildMppChallenge({ + id: 'ch_123', + options, + resource: 'https://music.mcp.atxp.ai/', + }); expect(result).toMatchObject({ id: 'ch_123', method: 'tempo', @@ -222,6 +226,13 @@ describe('omniChallenge', () => { currency: 'pathUSD', network: 'tempo', recipient: '0xTempo', + resource: { url: 'https://music.mcp.atxp.ai/' }, + request: { + amount: '0.01', + currency: 'pathUSD', + recipient: '0xTempo', + resource: { url: 'https://music.mcp.atxp.ai/' }, + }, }); // Tempo challenges include expires; Solana does not expect(result!.expires).toBeDefined(); @@ -493,8 +504,12 @@ describe('omniChallenge', () => { expect(result.mpp![0].method).toBe('solana'); expect(result.mpp![0].recipient).toBe('SolanaAddr123'); expect(result.mpp![0].id).toBe('ch_test_1'); + expect(result.mpp![0].resource).toEqual({ url: 'https://example.com/api' }); + expect(result.mpp![0].request?.resource).toEqual({ url: 'https://example.com/api' }); expect(result.mpp![1].method).toBe('tempo'); expect(result.mpp![1].recipient).toBe('0xTempoAddr'); + expect(result.mpp![1].resource).toEqual({ url: 'https://example.com/api' }); + expect(result.mpp![1].request?.resource).toEqual({ url: 'https://example.com/api' }); // Options: all three sources converted expect(result.options).toHaveLength(3); @@ -556,7 +571,11 @@ describe('omniChallenge', () => { expect(result.challenges).toHaveLength(2); expect(result.challenges[0].method).toBe('solana'); expect(result.challenges[0].id).toBe('ch_auth_1'); + expect(result.challenges[0].resource).toEqual({ url: 'https://example.com/resource' }); + expect(result.challenges[0].request?.resource).toEqual({ url: 'https://example.com/resource' }); expect(result.challenges[1].method).toBe('tempo'); + expect(result.challenges[1].resource).toEqual({ url: 'https://example.com/resource' }); + expect(result.challenges[1].request?.resource).toEqual({ url: 'https://example.com/resource' }); }); it('should omit paymentRequirements when no X402-compatible sources exist', () => { diff --git a/packages/atxp-server/src/omniChallenge.ts b/packages/atxp-server/src/omniChallenge.ts index dbad3a7..b339707 100644 --- a/packages/atxp-server/src/omniChallenge.ts +++ b/packages/atxp-server/src/omniChallenge.ts @@ -98,8 +98,10 @@ export function buildAtxpMcpChallenge( export function buildMppChallenges(args: { id: string; options: Array<{ network: string; currency: string; address: string; amount: BigNumber }>; + resource?: string; }): MppChallengeData[] | null { const challenges: MppChallengeData[] = []; + const resourceField = args.resource ? { resource: { url: args.resource } } : {}; // Solana option (USDC on Solana mainnet or devnet) // Amount in micro-units (e.g., 10000 = 0.01 USDC). @solana/mpp expects this. @@ -114,6 +116,13 @@ export function buildMppChallenges(args: { currency: USDC_ADDRESSES[isDevnet ? 'solana_devnet' : 'solana'], network: isDevnet ? 'devnet' : 'mainnet-beta', recipient: solanaOption.address, + ...resourceField, + request: { + amount: solanaOption.amount.times(10 ** STABLECOIN_DECIMALS).toFixed(0), + currency: USDC_ADDRESSES[isDevnet ? 'solana_devnet' : 'solana'], + recipient: solanaOption.address, + ...resourceField, + }, }); } @@ -133,6 +142,13 @@ export function buildMppChallenges(args: { network: tempoOption.network, recipient: tempoOption.address, expires: new Date(Date.now() + 5 * 60 * 1000).toISOString(), + ...resourceField, + request: { + amount: tempoOption.amount.toString(), + currency: tempoOption.currency || 'USDC', + recipient: tempoOption.address, + ...resourceField, + }, }); } @@ -146,6 +162,7 @@ export function buildMppChallenges(args: { export function buildMppChallenge(args: { id: string; options: Array<{ network: string; currency: string; address: string; amount: BigNumber }>; + resource?: string; }): MppChallengeData | null { const challenges = buildMppChallenges(args); return challenges?.[0] ?? null; @@ -274,7 +291,7 @@ export function buildOmniChallenge(args: { mppChallengeId?: string; }): OmniChallenge { const mpp = args.mppChallengeId - ? buildMppChallenges({ id: args.mppChallengeId, options: args.options }) + ? buildMppChallenges({ id: args.mppChallengeId, options: args.options, resource: args.resource }) : null; return { @@ -335,7 +352,7 @@ export function buildPaymentOptions(args: { resource: args.resource ?? '', payeeName: args.payeeName ?? '', }), - mpp: buildMppChallenges({ id: challengeId, options }), + mpp: buildMppChallenges({ id: challengeId, options, resource: args.resource }), options, }; } diff --git a/packages/atxp-server/src/protocol.ts b/packages/atxp-server/src/protocol.ts index c7928ed..9719059 100644 --- a/packages/atxp-server/src/protocol.ts +++ b/packages/atxp-server/src/protocol.ts @@ -26,6 +26,10 @@ export type MppChallengeData = { recipient: string; /** ISO 8601 expiry timestamp. Required by mppx's verify() for Tempo challenges. */ expires?: string; + /** Resource URL for the payee MCP server. Preserved through accounts/auth for activity labels. */ + resource?: { url: string }; + /** Nested request object. mppx credentials preserve request metadata through settlement. */ + request?: Record; /** Server-defined opaque data echoed by clients. Used to carry signed * identity when Authorization: Payment replaces Authorization: Bearer. */ opaque?: Record;