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
5 changes: 5 additions & 0 deletions .changeset/x402-bsc-payments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nansen-cli": patch
---

Support x402 payments with USDT on BNB Smart Chain (`eip155:56`), which the Nansen API now advertises as a payment option. Payment signing already handled any EVM network generically; this adds BSC to the post-payment balance check with the correct token contract and 18-decimal precision (Base USDC and X Layer USDT0 use 6), plus a `bsc` entry in the shared RPC registry with a `NANSEN_BSC_RPC` override.
38 changes: 38 additions & 0 deletions src/__tests__/x402-balance.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Tests for the x402 EVM payment-token registry used by balance checks.
*/

import { describe, it, expect } from 'vitest';
import { EVM_X402_TOKENS } from '../x402.js';
import { CHAIN_RPCS } from '../rpc-urls.js';

describe('EVM_X402_TOKENS registry', () => {
it('covers Base, X Layer, and BNB Smart Chain', () => {
expect(Object.keys(EVM_X402_TOKENS).sort()).toEqual(
['eip155:196', 'eip155:56', 'eip155:8453'],
);
});

it('uses 18 decimals for BSC USDT and 6 for the others', () => {
expect(EVM_X402_TOKENS['eip155:56'].decimals).toBe(18);
expect(EVM_X402_TOKENS['eip155:8453'].decimals).toBe(6);
expect(EVM_X402_TOKENS['eip155:196'].decimals).toBe(6);
});

it('pins the expected token contracts', () => {
expect(EVM_X402_TOKENS['eip155:8453'].token)
.toBe('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'); // Base USDC
expect(EVM_X402_TOKENS['eip155:56'].token)
.toBe('0x55d398326f99059fF775485246999027B3197955'); // BSC USDT
});

it('resolves an RPC URL for every network', () => {
for (const [network, cfg] of Object.entries(EVM_X402_TOKENS)) {
expect(cfg.rpc, `rpc for ${network}`).toMatch(/^https?:\/\//);
}
});

it('has a BSC entry in the shared RPC registry', () => {
expect(CHAIN_RPCS.bsc).toMatch(/^https?:\/\//);
});
});
3 changes: 3 additions & 0 deletions src/rpc-urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Override env vars:
* NANSEN_EVM_RPC Custom Ethereum RPC (also used as generic EVM fallback)
* NANSEN_BASE_RPC Custom Base RPC
* NANSEN_BSC_RPC Custom BNB Smart Chain RPC
* NANSEN_XLAYER_RPC Custom X Layer RPC
* NANSEN_SOLANA_RPC Custom Solana RPC
*
Expand All @@ -20,13 +21,15 @@

const DEFAULT_EVM_RPC = 'https://eth.public-rpc.com';
const DEFAULT_BASE_RPC = 'https://mainnet.base.org';
const DEFAULT_BSC_RPC = 'https://bsc-dataseed.binance.org';
const DEFAULT_XLAYER_RPC = 'https://rpc.xlayer.tech';
const DEFAULT_SOLANA_RPC = 'https://api.mainnet-beta.solana.com';

export const CHAIN_RPCS = {
ethereum: process.env.NANSEN_EVM_RPC || DEFAULT_EVM_RPC,
evm: process.env.NANSEN_EVM_RPC || DEFAULT_EVM_RPC, // generic EVM fallback
base: process.env.NANSEN_BASE_RPC || process.env.NANSEN_RPC_BASE || DEFAULT_BASE_RPC,
bsc: process.env.NANSEN_BSC_RPC || DEFAULT_BSC_RPC,
xlayer: process.env.NANSEN_XLAYER_RPC || DEFAULT_XLAYER_RPC,
solana: process.env.NANSEN_SOLANA_RPC || DEFAULT_SOLANA_RPC,
};
21 changes: 14 additions & 7 deletions src/x402.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,17 @@ export async function createPaymentSignature(response, url, options = {}) {
return null;
}

/**
* x402 payment tokens per EVM network, matching the stablecoins the API
* advertises in 402 `accepts` entries. `decimals` matters: USDT on BNB Smart
* Chain uses 18 decimals, unlike the 6-decimal tokens on Base and X Layer.
*/
export const EVM_X402_TOKENS = {
'eip155:8453': { token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', rpc: CHAIN_RPCS.base, symbol: 'USDC', decimals: 6 }, // Base USDC
'eip155:196': { token: '0x779Ded0c9e1022225f8E0630b35a9b54bE713736', rpc: CHAIN_RPCS.xlayer, symbol: 'USDT0', decimals: 6 }, // X Layer USDT0
'eip155:56': { token: '0x55d398326f99059fF775485246999027B3197955', rpc: CHAIN_RPCS.bsc, symbol: 'USDT', decimals: 18 }, // BSC USDT
};

/**
* Check stablecoin balance for x402 payment wallet on the given network.
* Returns `{ balance, symbol }` (USD amount + token symbol) or null if check fails.
Expand Down Expand Up @@ -192,13 +203,9 @@ export async function checkX402Balance(network) {
}

if (network.startsWith('eip155:')) {
// Per-network token + RPC. Both tokens are 6-decimals.
// Default to Base USDC if the network is unknown so existing wallets keep working.
const EVM_NETWORKS = {
'eip155:8453': { token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', rpc: CHAIN_RPCS.base, symbol: 'USDC' }, // Base USDC
'eip155:196': { token: '0x779Ded0c9e1022225f8E0630b35a9b54bE713736', rpc: CHAIN_RPCS.xlayer, symbol: 'USDT0' }, // X Layer USDT0
};
const { token, rpc, symbol } = EVM_NETWORKS[network] || EVM_NETWORKS['eip155:8453'];
const { token, rpc, symbol, decimals } =
EVM_X402_TOKENS[network] || EVM_X402_TOKENS['eip155:8453'];
const addr = walletInfo.evm.replace('0x', '').toLowerCase().padStart(64, '0');
const resp = await fetch(rpc, {
method: 'POST',
Expand All @@ -210,7 +217,7 @@ export async function checkX402Balance(network) {
}),
});
const data = await resp.json();
return { balance: parseInt(data.result, 16) / 1e6, symbol };
return { balance: parseInt(data.result, 16) / 10 ** decimals, symbol };
}

return null;
Expand Down
Loading