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
11,549 changes: 2,635 additions & 8,914 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions packages/atxp-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@
"@account-kit/infra": "^4.81.3",
"@atxp/client": "0.10.13",
"@atxp/common": "0.10.13",
"@x402/core": "^2.9.0",
"@x402/evm": "^2.9.0",
"bignumber.js": "^9.3.0",
"viem": "^2.34.0",
"x402": "^0.3.0"
"viem": "^2.34.0"
},
"devDependencies": {
"@types/node": "^25.0.3",
Expand Down
32 changes: 25 additions & 7 deletions packages/atxp-base/src/baseAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { privateKeyToAccount, PrivateKeyAccount } from 'viem/accounts';
import { BasePaymentMaker } from './basePaymentMaker.js';
import { createWalletClient, http, WalletClient, LocalAccount } from 'viem';
import { base } from 'viem/chains';
import { createPaymentHeader } from 'x402/client';
import { ExactEvmScheme, toClientEvmSigner } from '@x402/evm';
import { x402HTTPClient, x402Client } from '@x402/core/client';

export class BaseAccount implements Account {
readonly usesAccountsAuthorize = false;
Expand Down Expand Up @@ -89,14 +90,31 @@ export class BaseAccount implements Account {
throw new Error('BaseAccount: x402 authorize requires paymentRequirements');
}
const reqs = params.paymentRequirements as Record<string, unknown>;
const x402Version = (reqs.x402Version as number) || 1;
const x402Version = (reqs.x402Version as number) || 2;

// TODO: This x402 client bootstrap (scheme + client + httpClient + createPaymentPayload +
// encodePaymentSignatureHeader) is duplicated in x402Wrapper.ts. Extract a shared helper
// once both packages can import from a common location that depends on @x402/core + @x402/evm.
const signer = toClientEvmSigner(this.getLocalAccount());
const scheme = new ExactEvmScheme(signer);
const client = new x402Client();
// v2 uses CAIP-2 network IDs ("eip155:8453")
client.register(reqs.network as `${string}:${string}`, scheme);
const httpClient = new x402HTTPClient(client);

// Build PaymentRequired envelope from the enriched requirements
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const paymentHeader = await createPaymentHeader(
this.getLocalAccount(),
const paymentRequired = {
x402Version,
reqs as any,
);
return { protocol, credential: paymentHeader as string };
accepts: [reqs],
resource: { url: params.destination || '' },
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const paymentPayload = await httpClient.createPaymentPayload(paymentRequired as any);
const headers = httpClient.encodePaymentSignatureHeader(paymentPayload);
const paymentHeader = headers['PAYMENT-SIGNATURE'] || headers['X-PAYMENT'] || headers['x-payment'] || '';
return { protocol, credential: paymentHeader };
}
case 'atxp': {
if (!params.amount) {
Expand Down
25 changes: 0 additions & 25 deletions packages/atxp-base/src/x402-client.d.ts

This file was deleted.

5 changes: 3 additions & 2 deletions packages/atxp-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@
"@atxp/common": "0.10.13",
"@atxp/mpp": "0.10.13",
"@modelcontextprotocol/sdk": "^1.15.0",
"@x402/core": "^2.9.0",
"@x402/evm": "^2.9.0",
"bignumber.js": "^9.3.0",
"oauth4webapi": "^3.8.3",
"x402": "^1.1.0"
"oauth4webapi": "^3.8.3"
},
"peerDependencies": {
"expo-crypto": ">=14.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/atxp-client/src/atxpAccountHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe('ATXPAccountHandler', () => {
x402: {
accepts: [
{ network: 'atxp', payTo: 'atxp_acct_123' },
{ network: 'base', payTo: '0xDEST', maxAmountRequired: '1000000' },
{ network: 'eip155:8453', payTo: '0xDEST', amount: '1000000' },
],
},
});
Expand All @@ -143,7 +143,7 @@ describe('ATXPAccountHandler', () => {
expect.objectContaining({
destination: '0xDEST',
paymentRequirements: expect.objectContaining({
network: 'base',
network: 'eip155:8453',
payTo: '0xDEST',
}),
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/atxp-client/src/atxpAccountHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ async function buildAuthorizeParams(
};
if (chainOption.payTo) params.destination = chainOption.payTo as string;
if (chainOption.network) params.network = chainOption.network as string;
if (chainOption.maxAmountRequired && !params.amount) params.amount = chainOption.maxAmountRequired as string;
if (chainOption.amount && !params.amount) params.amount = chainOption.amount as string;
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/atxp-client/src/protocolHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ function createMockAccount(paymentMakers?: PaymentMaker[]): Account {

function createX402Challenge() {
return {
x402Version: 1,
x402Version: 2,
accepts: [{
network: 'base',
network: 'eip155:8453',
scheme: 'exact',
payTo: '0xrecipient',
maxAmountRequired: '1000000',
amount: '1000000',
description: 'Test payment',
}]
};
Expand Down
25 changes: 0 additions & 25 deletions packages/atxp-client/src/x402-client.d.ts

This file was deleted.

53 changes: 33 additions & 20 deletions packages/atxp-client/src/x402ProtocolHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,40 @@ import type { ProspectivePayment } from './types.js';
import { ATXPPaymentError } from './errors.js';
import { BigNumber } from 'bignumber.js';
import { buildPaymentHeaders } from './paymentHeaders.js';
import { selectPaymentRequirements } from 'x402/client';

/** USDC contract addresses by network, used to enrich X402 payment requirements.
* Source: https://developers.circle.com/stablecoins/usdc-on-main-networks */
const USDC_ADDRESSES: Record<string, string> = {
base: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
base_sepolia: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
};
import { USDC_ADDRESSES } from '@atxp/common';

/**
* Type guard for X402 challenge body.
* Type guard for X402 challenge body (supports v1 and v2).
*/
interface X402ChallengeAccept {
network: string;
scheme: string;
payTo: string;
amount: string | number;
description?: string;
asset?: string;
mimeType?: string;
maxTimeoutSeconds?: number;
extra?: Record<string, unknown>;
}

interface X402Challenge {
x402Version: number;
accepts: Array<{
network: string;
scheme: string;
payTo: string;
maxAmountRequired: string | number;
description?: string;
}>;
accepts: X402ChallengeAccept[];
/** v2 adds resource info and extensions */
resource?: { url: string; description?: string; mimeType?: string };
extensions?: Record<string, unknown>;
}

/**
* Select the first payment requirement matching the 'exact' scheme.
* Replaces the old `selectPaymentRequirements` from x402 v1.
*/
function selectPaymentRequirements(
accepts: X402ChallengeAccept[],
preferredScheme = 'exact',
): X402ChallengeAccept | undefined {
return accepts.find(a => a.scheme === preferredScheme) ?? accepts[0];
}

function isX402Challenge(obj: unknown): obj is X402Challenge {
Expand Down Expand Up @@ -81,7 +94,6 @@ export class X402ProtocolHandler implements ProtocolHandler {
try {
const selectedPaymentRequirements = selectPaymentRequirements(
paymentChallenge.accepts,
undefined,
'exact'
);

Expand All @@ -90,7 +102,7 @@ export class X402ProtocolHandler implements ProtocolHandler {
return this.reconstructResponse(responseBody, response);
}

const amountInUsdc = Number(selectedPaymentRequirements.maxAmountRequired) / (10 ** 6);
const amountInUsdc = Number(selectedPaymentRequirements.amount) / (10 ** 6);
const network = selectedPaymentRequirements.network;
logger.debug(`X402: payment required: ${amountInUsdc} USDC on ${network} to ${selectedPaymentRequirements.payTo}`);

Expand Down Expand Up @@ -124,7 +136,8 @@ export class X402ProtocolHandler implements ProtocolHandler {
// for accounts that sign locally (e.g., BaseAccount).
const enrichedRequirements = {
...selectedPaymentRequirements,
asset: selectedPaymentRequirements.asset || USDC_ADDRESSES[network] || USDC_ADDRESSES['base'],
x402Version: paymentChallenge.x402Version,
asset: selectedPaymentRequirements.asset || USDC_ADDRESSES[network] || USDC_ADDRESSES['eip155:8453'],
mimeType: selectedPaymentRequirements.mimeType || 'application/json',
};

Expand Down Expand Up @@ -174,7 +187,7 @@ export class X402ProtocolHandler implements ProtocolHandler {

if (isX402Challenge(paymentChallenge) && paymentChallenge.accepts[0]) {
const firstOption = paymentChallenge.accepts[0];
const amount = firstOption.maxAmountRequired ? Number(firstOption.maxAmountRequired) / (10 ** 6) : 0;
const amount = Number(firstOption.amount) / (10 ** 6);
const url = typeof originalRequest.url === 'string' ? originalRequest.url : originalRequest.url.toString();
const accountId = await account.getAccountId();
const errorNetwork = firstOption.network || 'unknown';
Expand Down
24 changes: 24 additions & 0 deletions packages/atxp-common/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* USDC contract addresses by network.
*
* Source: https://developers.circle.com/stablecoins/usdc-on-main-networks
*
* Includes both human-readable network names (e.g. "base") and CAIP-2
* identifiers (e.g. "eip155:8453") for convenience.
*/
export const USDC_ADDRESSES: Record<string, string> = {
'base': '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
'base_sepolia': '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
'eip155:8453': '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
'eip155:84532': '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
};

/**
* CAIP-2 network identifiers for EVM chains supported by the CDP facilitator.
*
* Source: https://docs.cdp.coinbase.com/x402/network-support
*/
export const CAIP2_NETWORKS: Record<string, string> = {
base: 'eip155:8453',
base_sepolia: 'eip155:84532',
};
3 changes: 3 additions & 0 deletions packages/atxp-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ export {
extractNetworkFromAccountId
} from './types.js';

// Constants (USDC addresses, CAIP-2 network IDs)
export { USDC_ADDRESSES, CAIP2_NETWORKS } from './constants.js';

// Utility functions
export {
assertNever,
Expand Down
10 changes: 8 additions & 2 deletions packages/atxp-express/src/atxpExpress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function atxpExpress(args: ATXPArgs): Router {
// with full pricing context (amount, options, destination).
const detected = detectProtocol({
'x-atxp-payment': req.headers['x-atxp-payment'] as string | undefined,
'payment-signature': req.headers['payment-signature'] as string | undefined,
'x-payment': req.headers['x-payment'] as string | undefined,
'authorization': req.headers['authorization'] as string | undefined,
});
Expand Down Expand Up @@ -75,13 +76,18 @@ export function atxpExpress(args: ATXPArgs): Router {
// before charging, using the pricing context it has (amount, options).
return withATXPContext(config, resource, tokenCheck, () => {
if (detected) {
const sourceAccountId = resolveIdentitySync(config, req, detected.protocol, detected.credential);
// Resolve identity from the credential itself (ATXP/MPP embed the source),
// then fall back to the OAuth sub. This is critical for X402: the credential
// doesn't contain the user's identity, but the OAuth token does. The settle
// must use the same sourceAccountId as the charge (atxpAccountId() = OAuth sub)
// so the ledger entries match.
const sourceAccountId = resolveIdentitySync(config, req, detected.protocol, detected.credential) || user || undefined;
setDetectedCredential({
protocol: detected.protocol,
credential: detected.credential,
sourceAccountId,
});
logger.info(`Stored ${detected.protocol} credential in context for requirePayment`);
logger.info(`Stored ${detected.protocol} credential in context for requirePayment (sourceAccountId=${sourceAccountId})`);
}
return next();
});
Expand Down
31 changes: 31 additions & 0 deletions packages/atxp-express/src/omniChallenge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,37 @@ describe('credential detection Express middleware', () => {
expect(storedCredential!.sourceAccountId).toBe('atxp_acct_raw123');
});

it('should store X402 credential with sourceAccountId from OAuth sub (fallback)', async () => {
let storedCredential: DetectedCredential | null = null;

const router = atxpExpress(TH.config({
oAuthClient: TH.oAuthClient({ introspectResult: TH.tokenData({ active: true, sub: 'atxp:atxp_acct_x402user' }) }),
}));

const app = express();
app.use(express.json());
app.use(router);
app.post('/', (_req, res) => {
storedCredential = getDetectedCredential();
res.json({ ok: true });
});

const response = await request(app)
.post('/')
.set('Content-Type', 'application/json')
.set('X-PAYMENT', 'x402-payment-credential')
.set('Authorization', 'Bearer test-token')
.send(TH.mcpToolRequest());

expect(response.status).toBe(200);
expect(storedCredential).not.toBeNull();
expect(storedCredential!.protocol).toBe('x402');
// X402 credentials don't contain identity, so sourceAccountId falls back
// to the OAuth sub. This ensures the settle credits the same account that
// the charge deducts from (atxpAccountId() = OAuth sub).
expect(storedCredential!.sourceAccountId).toBe('atxp:atxp_acct_x402user');
});

it('should store ATXP credential with sourceAccountId from base64-encoded JSON', async () => {
let storedCredential: DetectedCredential | null = null;

Expand Down
Loading
Loading