From a1aa3195ac3114c3ed3a112df36e9159a14396e6 Mon Sep 17 00:00:00 2001 From: Rob Di Marco Date: Mon, 12 Jan 2026 15:21:46 -0500 Subject: [PATCH 1/2] Fix: Use iss field for payment memo instead of payeeName Resolves ATXP-1204 - Bad memo for proxy transactions When making payments through the MCP proxy, the transaction memo was being set to the MCP server name (e.g., "Image") instead of the auth server ID (e.g., "auth.atxp.ai"), causing the auth server to reject payments. Changes: - Add optional iss field to PaymentRequest type in atxp-common - Update ATXPFetcher to use paymentRequest.iss as memo with fallback to paymentRequest.payeeName for backward compatibility - This ensures transaction memos match what auth server expects The auth server sends iss: ATXP_AUTH_SERVER_ID in payment requests, which should be used as the transaction memo for validation. --- packages/atxp-client/src/atxpFetcher.ts | 5 +++-- packages/atxp-common/src/types.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/atxp-client/src/atxpFetcher.ts b/packages/atxp-client/src/atxpFetcher.ts index b7138a5..ec6320d 100644 --- a/packages/atxp-client/src/atxpFetcher.ts +++ b/packages/atxp-client/src/atxpFetcher.ts @@ -228,7 +228,7 @@ export class ATXPFetcher { resourceName: paymentRequest.payeeName ?? '', currency: firstDest.currency, amount: firstDest.amount, - iss: paymentRequest.payeeName ?? '', + iss: paymentRequest.iss ?? paymentRequest.payeeName ?? '', }; // Ask for approval once for all payment attempts @@ -246,7 +246,8 @@ export class ATXPFetcher { for (const paymentMaker of this.account.paymentMakers) { try { // Pass all destinations to payment maker - it will filter and pick the one it can handle - const result = await paymentMaker.makePayment(mappedDestinations, paymentRequest.payeeName ?? '', paymentRequestId); + const memo = paymentRequest.iss ?? paymentRequest.payeeName ?? ''; + const result = await paymentMaker.makePayment(mappedDestinations, memo, paymentRequestId); if (result === null) { this.logger.debug(`ATXP: payment maker cannot handle these destinations, trying next`); diff --git a/packages/atxp-common/src/types.ts b/packages/atxp-common/src/types.ts index 6e796c4..8232576 100644 --- a/packages/atxp-common/src/types.ts +++ b/packages/atxp-common/src/types.ts @@ -85,6 +85,7 @@ export type PaymentRequest = { destinationAccountId: AccountId; resource: URL; payeeName: string | null; + iss?: string; } export type CustomJWTPayload = { From aa3820b6ac188409036f554bd54c0e1447d48136 Mon Sep 17 00:00:00 2001 From: Rob Di Marco Date: Mon, 12 Jan 2026 15:37:18 -0500 Subject: [PATCH 2/2] Add tests for iss field usage in payment memo Add two comprehensive tests to verify the fix: 1. When iss field is present in payment request, use it as memo 2. When iss field is absent, fall back to payeeName for backward compatibility These tests ensure the proxy transaction memo issue is properly resolved and that backward compatibility is maintained for older payment requests. --- .../src/atxpFetcher.payment.test.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/packages/atxp-client/src/atxpFetcher.payment.test.ts b/packages/atxp-client/src/atxpFetcher.payment.test.ts index 6b7f29d..789b154 100644 --- a/packages/atxp-client/src/atxpFetcher.payment.test.ts +++ b/packages/atxp-client/src/atxpFetcher.payment.test.ts @@ -314,4 +314,88 @@ describe('atxpFetcher.fetch payment', () => { } expect(threw).toBe(true); }); + + it('should use iss field as memo when present in payment request', async () => { + const f = fetchMock.createInstance(); + const errTxt = CTH.paymentRequiredMessage(DEFAULT_AUTHORIZATION_SERVER, 'foo'); + const errMsg = CTH.mcpToolErrorResponse({content: [{type: 'text', text: errTxt}]}); + + mockResourceServer(f, 'https://example.com', '/mcp', DEFAULT_AUTHORIZATION_SERVER) + .postOnce('https://example.com/mcp', errMsg) + .postOnce('https://example.com/mcp', {content: [{type: 'text', text: 'hello world'}]}); + + // Mock payment request with both iss and payeeName + mockAuthorizationServer(f, DEFAULT_AUTHORIZATION_SERVER, {}); + f.getOnce(`${DEFAULT_AUTHORIZATION_SERVER}/payment-request/foo`, { + options: [{ + amount: '0.01', + currency: 'USDC', + network: 'solana', + address: 'testDestination' + }], + sourceAccountId: 'solana:testSource', + destinationAccountId: 'solana:testDestination', + resource: new URL('https://example.com/resource'), + payeeName: 'Image', + iss: 'auth.atxp.ai' + }); + f.putOnce(`${DEFAULT_AUTHORIZATION_SERVER}/payment-request/foo`, 200); + + const paymentMaker = { + makePayment: vi.fn().mockResolvedValue({ transactionId: 'testPaymentId', chain: 'solana', currency: 'USDC' }), + generateJWT: vi.fn().mockResolvedValue('testJWT'), + getSourceAddress: vi.fn().mockReturnValue('SolAddress123') + }; + const fetcher = atxpFetcher(f.fetchHandler, [paymentMaker]); + await fetcher.fetch('https://example.com/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); + + // Verify makePayment was called with iss as memo, not payeeName + expect(paymentMaker.makePayment).toHaveBeenCalledWith( + expect.anything(), // destinations + 'auth.atxp.ai', // memo should be iss value + 'foo' // paymentRequestId + ); + }); + + it('should fall back to payeeName as memo when iss is not present in payment request', async () => { + const f = fetchMock.createInstance(); + const errTxt = CTH.paymentRequiredMessage(DEFAULT_AUTHORIZATION_SERVER, 'bar'); + const errMsg = CTH.mcpToolErrorResponse({content: [{type: 'text', text: errTxt}]}); + + mockResourceServer(f, 'https://example.com', '/mcp', DEFAULT_AUTHORIZATION_SERVER) + .postOnce('https://example.com/mcp', errMsg) + .postOnce('https://example.com/mcp', {content: [{type: 'text', text: 'hello world'}]}); + + // Mock payment request with only payeeName (no iss field) + mockAuthorizationServer(f, DEFAULT_AUTHORIZATION_SERVER, {}); + f.getOnce(`${DEFAULT_AUTHORIZATION_SERVER}/payment-request/bar`, { + options: [{ + amount: '0.01', + currency: 'USDC', + network: 'solana', + address: 'testDestination' + }], + sourceAccountId: 'solana:testSource', + destinationAccountId: 'solana:testDestination', + resource: new URL('https://example.com/resource'), + payeeName: 'LegacyService' + // Note: no iss field + }); + f.putOnce(`${DEFAULT_AUTHORIZATION_SERVER}/payment-request/bar`, 200); + + const paymentMaker = { + makePayment: vi.fn().mockResolvedValue({ transactionId: 'testPaymentId', chain: 'solana', currency: 'USDC' }), + generateJWT: vi.fn().mockResolvedValue('testJWT'), + getSourceAddress: vi.fn().mockReturnValue('SolAddress123') + }; + const fetcher = atxpFetcher(f.fetchHandler, [paymentMaker]); + await fetcher.fetch('https://example.com/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); + + // Verify makePayment was called with payeeName as memo (backward compatibility) + expect(paymentMaker.makePayment).toHaveBeenCalledWith( + expect.anything(), // destinations + 'LegacyService', // memo should be payeeName when iss not present + 'bar' // paymentRequestId + ); + }); });