Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/atxp-server/src/omniChallenge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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', () => {
Expand Down
21 changes: 19 additions & 2 deletions packages/atxp-server/src/omniChallenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
},
});
}

Expand All @@ -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,
},
});
}

Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
};
}
Expand Down
4 changes: 4 additions & 0 deletions packages/atxp-server/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
/** Server-defined opaque data echoed by clients. Used to carry signed
* identity when Authorization: Payment replaces Authorization: Bearer. */
opaque?: Record<string, unknown>;
Expand Down
Loading