Skip to content
Draft
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
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"db:cleanup": "node dist/cron/dbCleanup.js",
"funnel-report": "node dist/scripts/funnelReport.js",
"npm:audit": "npm audit --json > /tmp/audit.json && echo \"Audit complete\"",
"test": "TS_NODE_TRANSPILE_ONLY=1 node --loader ts-node/esm tests/wallet-provisioning.test.js && node --loader ts-node/esm tests/ssrf.test.js && node tests/integration.test.js && node tests/pages.test.js && node tests/x402-v1-passthrough.test.mjs && node tests/model-cost.test.mjs && node tests/session-pricing.test.mjs && node tests/prompt-moderation.test.mjs && node tests/critical-regressions.test.mjs && node tests/unsubscribe.test.mjs && node tests/reactivation-render.test.mjs && node tests/outreach-active-devs.test.mjs && node tests/credit-alert-dedup.test.mjs && node tests/verify-activation.test.mjs && node tests/signup-firstcall.test.mjs && node tests/oauth-signup-cta.test.mjs && node tests/x402-sell-copy.test.mjs && node tests/verify-resend.test.mjs && node tests/intent-funnel.test.mjs && node tests/credit-email-buylinks.test.mjs",
"test": "TS_NODE_TRANSPILE_ONLY=1 node --loader ts-node/esm tests/wallet-provisioning.test.js && node --loader ts-node/esm tests/ssrf.test.js && node tests/integration.test.js && node tests/pages.test.js && node tests/x402-v1-passthrough.test.mjs && node tests/model-cost.test.mjs && node tests/session-pricing.test.mjs && node tests/prompt-moderation.test.mjs && node tests/critical-regressions.test.mjs && node tests/unsubscribe.test.mjs && node tests/reactivation-render.test.mjs && node tests/outreach-active-devs.test.mjs && node tests/credit-alert-dedup.test.mjs && node tests/verify-activation.test.mjs && node tests/signup-firstcall.test.mjs && node tests/oauth-signup-cta.test.mjs && node tests/x402-sell-copy.test.mjs && node tests/x402-discovery-rails.test.mjs && node tests/verify-resend.test.mjs && node tests/intent-funnel.test.mjs && node tests/credit-email-buylinks.test.mjs",
"test:integration": "node tests/integration.test.js",
"test:verify-activation": "node tests/verify-activation.test.mjs",
"test:verify-resend": "node tests/verify-resend.test.mjs",
Expand Down
82 changes: 39 additions & 43 deletions api/src/middleware/x402.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,44 @@ const USDT_CONTRACTS: Record<string, string> = {
bsc: "0x55d398326f99059fF775485246999027B3197955", // USDT on BSC (BEP-20) — $40B+ liquidity
};

// CDP Facilitator supported networks ONLY.
// CDP supports: Base, Polygon, Solana — for ERC-20 tokens (USDC via EIP-3009, any ERC-20 via Permit2).
// CDP does NOT support: Avalanche, Ethereum mainnet, Arbitrum, Optimism, native ETH, native SOL, or any non-ERC-20.
// Source: https://docs.cdp.coinbase.com/x402/network-support
const CDP_SUPPORTED_NETWORKS = new Set([
"base",
"base-sepolia",
"eip155:8453",
"eip155:84532",
"polygon",
"polygon-amoy",
"eip155:137",
"eip155:80002",
"solana",
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"solana-devnet",
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
]);

export function filterCdpSupportedAccepts<T extends Record<string, any>>(accepts: T[]): T[] {
return accepts
.map((a) => {
// Normalize Solana to full CAIP-2 — @x402/svm registerV1 uses this exact string.
if (a.network === "solana:mainnet" || a.network === "solana") {
return { ...a, network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" };
}
return a;
})
.filter((a) => {
if (!CDP_SUPPORTED_NETWORKS.has(a.network)) return false;
if (a.asset === "native") return false; // native SOL — CDP can't handle
if (a.asset === "0x0000000000000000000000000000000000000000") return false; // native ETH
if (a.extra?.version === "native" || a.extra?.version === "native-base") return false; // no native ETH/SOL
if (a.extra?.version === "usol-erc20" || a.asset === "0x311935cd80b76769bf2ecc9d8ab7635b2139cf82") return false;
return true;
});
}

// Aptos native USDC token address (Circle native, launched Jan 2025)
const APTOS_USDC_ADDRESS = "0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b";

Expand Down Expand Up @@ -718,49 +756,7 @@ export function buildPaymentRequired(toolName: string, price: string): object {
}
}

// CDP Facilitator supported networks ONLY.
// CDP supports: Base, Polygon, Solana — for ERC-20 tokens (USDC via EIP-3009, any ERC-20 via Permit2).
// CDP does NOT support: Avalanche, Ethereum mainnet, Arbitrum, Optimism, native ETH, native SOL, or any non-ERC-20.
// Source: https://docs.cdp.coinbase.com/x402/network-support
// CDP supports ONLY these exact network identifiers. Named aliases (e.g. "base", "polygon") are NOT
// used here intentionally — they would also match native ETH/SOL options that CDP cannot handle.
// All CDP-supported options in buildPaymentRequired use CAIP-2 format (eip155:*) or "solana".
// CDP supports these networks. Use x402 named format (base/polygon/solana) in accepts[].
// CAIP-2 (eip155:*) variants included for internal verify/settle calls that may use them.
const CDP_SUPPORTED_NETWORKS = new Set([
"base", // Base mainnet ✅ (named — used in accepts[])
"base-sepolia", // Base Sepolia ✅
"eip155:8453", // Base mainnet ✅ (CAIP-2 — used in verify/settle)
"eip155:84532", // Base Sepolia ✅
"polygon", // Polygon mainnet ✅ (named — used in accepts[])
"polygon-amoy", // Polygon Amoy ✅
"eip155:137", // Polygon mainnet ✅ (CAIP-2 — used in verify/settle)
"eip155:80002", // Polygon Amoy ✅
"solana", // Solana mainnet (short alias) ✅
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", // Solana mainnet (full CAIP-2) ✅
"solana-devnet", // Solana devnet ✅
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", // Solana devnet (full CAIP-2) ✅
]);

// Filter to only CDP-supported networks.
// Normalize Solana network to full CAIP-2 format required by @x402/svm client SDK.
const filteredAccepts = accepts
.map((a: any) => {
// Normalize Solana to full CAIP-2 — @x402/svm registerV1 uses this exact string
if (a.network === "solana:mainnet" || a.network === "solana") {
a.network = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
}
return a;
})
.filter((a: any) => {
if (!CDP_SUPPORTED_NETWORKS.has(a.network)) return false;
if (a.asset === "native") return false; // native SOL — CDP can't handle
if (a.asset === "0x0000000000000000000000000000000000000000") return false; // native ETH
if (a.extra?.version === "native" || a.extra?.version === "native-base") return false; // no native ETH/SOL
if (a.extra?.version === "usol-erc20") return false; // no wrapped SOL on Base — CDP can't verify this
if (a.extra?.version === "usol-erc20" || a.asset === "0x311935cd80b76769bf2ecc9d8ab7635b2139cf82") return false;
return true;
});
const filteredAccepts = filterCdpSupportedAccepts(accepts as Array<Record<string, any>>);

return {
x402Version: 1,
Expand Down
25 changes: 8 additions & 17 deletions api/src/routes/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Router, Request, Response } from "express";
import { prisma } from "../lib/prisma.js";
import { redis } from "../lib/redis.js";
import { X402_PRICES } from "../middleware/x402.js";
import { X402_PRICES, filterCdpSupportedAccepts } from "../middleware/x402.js";
import { getStatusPageData } from "../middleware/analytics.js";
import { config } from "../config.js";
import { isValidAdminKey } from "../middleware/auth.js";
Expand Down Expand Up @@ -144,7 +144,7 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
const suiWallet = process.env.SUI_WALLET_ADDRESS ?? "";
const polkadotWallet = process.env.POLKADOT_WALLET_ADDRESS ?? "";
const aptosWallet = process.env.APTOS_WALLET_ADDRESS ?? "";
const usdtWallet = process.env.USDT_ETH_WALLET_ADDRESS ?? "";
const usdtWallet = evmWallet;
const ethWallet = process.env.ETH_WALLET_ADDRESS ?? "";
const bnbWallet = process.env.BNB_WALLET_ADDRESS ?? "";
const nearWallet = process.env.NEAR_WALLET_ADDRESS ?? "";
Expand Down Expand Up @@ -172,8 +172,7 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
};

// Build accepts array from live wallet config
const accepts: object[] = [];
const activeNetworks: string[] = [];
const accepts: Array<Record<string, unknown>> = [];

// USDC on EVM chains (Base, Ethereum, Arbitrum, Polygon, Optimism, Avalanche, Unichain, Monad)
if (evmWallet) {
Expand All @@ -189,7 +188,6 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
];
for (const { network, chain, description } of evmUsdcChains) {
accepts.push({ scheme: "exact", network, asset: USDC_CONTRACTS[chain], payTo: evmWallet, token: "USDC", description });
activeNetworks.push(network);
}
}

Expand All @@ -204,7 +202,6 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
token: "USDC",
description: "USDC on Solana (fast, cheap)",
});
activeNetworks.push("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
}

// USDC on Noble/Cosmos
Expand All @@ -217,7 +214,6 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
token: "USDC",
description: "USDC on Noble/Cosmos (IBC to 50+ Cosmos chains)",
});
activeNetworks.push("cosmos:noble-1");
}

// USDC on Algorand
Expand All @@ -230,7 +226,6 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
token: "USDC",
description: "USDC on Algorand (ASA #31566704)",
});
activeNetworks.push("algorand:mainnet");
}

// USDC on Stellar
Expand All @@ -244,7 +239,6 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
description: "USDC on Stellar (17-sec settlement)",
memo: stellarMemo || undefined,
});
activeNetworks.push("stellar:pubnet");
}

// USDC on Sui
Expand All @@ -257,7 +251,6 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
token: "USDC",
description: "USDC on Sui (Move L1, Circle CCTP)",
});
activeNetworks.push("sui:mainnet");
}

// USDC on Polkadot Asset Hub
Expand All @@ -270,7 +263,6 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
token: "USDC",
description: "USDC on Polkadot Asset Hub (XCM to all parachains)",
});
activeNetworks.push("polkadot:asset-hub");
}

// USDC on Aptos
Expand All @@ -283,7 +275,6 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
token: "USDC",
description: "USDC on Aptos (Move L1, native Circle)",
});
activeNetworks.push("aptos:mainnet");
}

// USDT on EVM chains
Expand Down Expand Up @@ -321,13 +312,11 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
// Native BNB
if (bnbWallet) {
accepts.push({ scheme: "exact", network: "eip155:56", asset: "0x0000000000000000000000000000000000000000", payTo: bnbWallet, token: "BNB", description: "Native BNB on BNB Chain" });
activeNetworks.push("eip155:56");
}

// Native NEAR
if (nearWallet) {
accepts.push({ scheme: "exact", network: "near:mainnet", asset: "near", payTo: nearWallet, token: "NEAR", description: "Native NEAR on NEAR Protocol" });
activeNetworks.push("near:mainnet");
}

// Native SOL
Expand All @@ -338,7 +327,6 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
// TAO (Bittensor)
if (taoWallet) {
accepts.push({ scheme: "exact", network: "bittensor:finney", asset: "TAO", payTo: taoWallet, token: "TAO", description: "TAO on Bittensor (AI-native blockchain)" });
activeNetworks.push("bittensor:finney");
}

// UNI (Uniswap)
Expand All @@ -352,6 +340,9 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
price: `$${price}`,
description: TOOL_DESCRIPTIONS[tool] ?? tool,
}));
const supportedRails = filterCdpSupportedAccepts(accepts);
const supportedNetworks = [...new Set(supportedRails.map((rail) => String(rail.network)))];
const supportedTokens = [...new Set(supportedRails.map((rail) => String(rail.token)).filter(Boolean))];

res.json({
name: "Arch Tools",
Expand All @@ -371,13 +362,13 @@ router.get("/.well-known/x402", (_req: Request, res: Response): void => {
// mention in coinbase/x402 specs/) and x402scan's discovery parser dropped it
// (@agentcash/discovery 1.7.5 SPECIFICATION.md: "Legacy /.well-known/x402 ...
// no longer parsed" — it discovers via /openapi.json).
supportedRails: accepts,
supportedRails,
rails_note:
"supportedRails is a chain/asset/wallet summary for humans and crawlers. Spec-shaped x402 v2 PaymentRequirements (CAIP-2 network, amount, maxTimeoutSeconds) are served in the accepts[] of each tool's 402 challenge.",
endpoints,
payment: {
stripe: { url: `${BASE_URL}/pricing` },
x402: { status: "active", networks: [...new Set(activeNetworks)], token: "USDC/USDT/ETH/BNB/NEAR/SOL/TAO/UNI" },
x402: { status: "active", networks: supportedNetworks, token: supportedTokens.join("/") || "USDC" },
},
mcp: {
server: "arch-tools-mcp",
Expand Down
79 changes: 79 additions & 0 deletions api/tests/x402-discovery-rails.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* x402 discovery rails — regression test for /.well-known/x402.
*
* The discovery endpoint must not advertise rails that the real x402 402
* challenge filters out before verify/settle. This mounts the compiled router
* with deliberately over-broad wallet envs and asserts the public metadata only
* exposes CDP-supported rails.
*
* Run: cd api && npm run build && node tests/x402-discovery-rails.test.mjs
*/
import assert from "assert";
import express from "express";

process.env.PUBLIC_SITE_URL = "https://archtools.dev";
process.env.WALLET_ADDRESS = "0x2583aAc89f58a63D9CCbeDaa5e3BaF2196Aa967e";
process.env.SOLANA_WALLET_ADDRESS = "D6ZhtNQ5nT9ZnTHUbqXZsTx5MH2rPFiBBggX4hY1WePM";
process.env.NOBLE_WALLET_ADDRESS = "noble1unsupported";
process.env.ALGORAND_WALLET_ADDRESS = "ALGO_UNSUPPORTED";
process.env.STELLAR_WALLET_ADDRESS = "STELLAR_UNSUPPORTED";
process.env.SUI_WALLET_ADDRESS = "0xsuiunsupported";
process.env.POLKADOT_WALLET_ADDRESS = "polkadotUnsupported";
process.env.APTOS_WALLET_ADDRESS = "0xaptosunsupported";
process.env.ETH_WALLET_ADDRESS = "0x2583aAc89f58a63D9CCbeDaa5e3BaF2196Aa967e";
process.env.BNB_WALLET_ADDRESS = "0x2583aAc89f58a63D9CCbeDaa5e3BaF2196Aa967e";
process.env.NEAR_WALLET_ADDRESS = "nearunsupported";
process.env.SOL_NATIVE_WALLET_ADDRESS = "SOL_UNSUPPORTED";
process.env.TAO_WALLET_ADDRESS = "taounsupported";
process.env.UNI_WALLET_ADDRESS = "0x2583aAc89f58a63D9CCbeDaa5e3BaF2196Aa967e";

const { default: discoveryRouter } = await import("../dist/routes/discovery.js");

const app = express();
app.use(discoveryRouter);
const server = app.listen(0);

try {
const { port } = server.address();
const res = await fetch(`http://127.0.0.1:${port}/.well-known/x402`);
assert.strictEqual(res.status, 200);
const body = await res.json();
const rails = body.supportedRails;
assert.ok(Array.isArray(rails), "supportedRails must be an array");

const railNetworks = rails.map((r) => r.network);
const networkSet = new Set(railNetworks);
assert.ok(networkSet.has("eip155:8453"), "Base USDC/USDT rail missing");
assert.ok(networkSet.has("eip155:137"), "Polygon USDC/USDT rail missing");
assert.ok(networkSet.has("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"), "Solana USDC rail missing");

const forbiddenNetworks = new Set([
"eip155:1",
"eip155:42161",
"eip155:10",
"eip155:43114",
"eip155:130",
"eip155:143",
"cosmos:noble-1",
"algorand:mainnet",
"stellar:pubnet",
"sui:mainnet",
"polkadot:asset-hub",
"aptos:mainnet",
"eip155:56",
"near:mainnet",
"bittensor:finney",
]);
for (const rail of rails) {
assert.ok(!forbiddenNetworks.has(rail.network), `unsupported network leaked: ${rail.network}`);
assert.notStrictEqual(rail.asset, "native", `native asset leaked on ${rail.network}`);
assert.notStrictEqual(rail.asset, "0x0000000000000000000000000000000000000000", `native EVM asset leaked on ${rail.network}`);
}

const summaryNetworks = body.payment?.x402?.networks ?? [];
assert.deepStrictEqual(new Set(summaryNetworks), networkSet, "payment.x402.networks must mirror filtered rails");
assert.strictEqual(body.payment?.x402?.token, "USDC/USDT");
console.log("x402-discovery-rails: ALL PASS");
} finally {
await new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
}
Loading