import test from 'node:test';
import assert from 'node:assert/strict';
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactEvmScheme } from '@x402/evm';
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
import { validateDiscoveryExtension } from '@x402/extensions/bazaar';
import { makeApp, seededStore, BASE, PAY_TO, mockHL, A } from './helpers.js';
import { headAsGet } from '../src/head.js';
import { handle as stdioHandle, TOOLS as STDIO_TOOLS } from '../src/mcp-stdio.js';
import { toSymbol, toAddress } from '../src/lite.js';
import { refresh, memoryStub, makeStore } from '../src/store.js';
const decode = (h) => JSON.parse(Buffer.from(h, 'base64').toString('utf8'));
const v2pay = (fetchApp) => wrapFetchWithPaymentFromConfig(fetchApp, { schemes: [{ network: 'eip155:8453', client: new ExactEvmScheme(privateKeyToAccount(generatePrivateKey())) }] });
const json = (body) => ({ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
const ROUTES = [['lite', '3000', 'symbol=BTC'], ['coin', '20000', 'symbol=ETH'], ['scan', '30000', 'limit=5'], ['wallet', '10000', `address=${A(2)}`]];
const NFA = /Not financial advice\. Past performance does not guarantee future results\./;
const mcp = (fetchApp, body) => fetchApp(`${BASE}/mcp`, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, ...body }) });
test('unpaid: every paid route answers 402 on GET, POST, empty POST and bare probes (v2 header <= 8192 B + valid Bazaar, v1 body); no upstream call', async () => {
const net = mockHL(); const { fetchApp } = await makeApp({ net }); const before = net.calls.length;
for (const [tier, amount, qs] of ROUTES) {
for (const [u, init] of [[`${BASE}/v1/${tier}?${qs}`], [`${BASE}/v1/${tier}`], [`${BASE}/v1/${tier}`, json(Object.fromEntries(new URLSearchParams(qs)))], [`${BASE}/v1/${tier}`, { method: 'POST' }], [`${BASE}/v1/${tier}`, json({})]]) {
const r = await fetchApp(u, init);
assert.equal(r.status, 402, `${init?.method || 'GET'} ${u}`);
const req = decode(r.headers.get('PAYMENT-REQUIRED')); const a = req.accepts[0];
assert.deepEqual([req.x402Version, a.scheme, a.network, a.amount, a.payTo, a.asset], [2, 'exact', 'eip155:8453', amount, PAY_TO, '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913']);
assert.equal(req.resource.url, `${BASE}/v1/${tier}`);
assert.ok(validateDiscoveryExtension(req.extensions.bazaar).valid, 'bazaar declaration valid');
assert.ok(r.headers.get('PAYMENT-REQUIRED').length <= 8192, 'header size');
const b = await r.json(); assert.equal(b.x402Version, 1); assert.equal(b.accepts[0].maxAmountRequired, amount); assert.equal(b.accepts[0].network, 'base');
}
}
assert.equal(net.calls.length, before);
});
test('HEAD = 402 + PAYMENT-REQUIRED with no body and never settles', async () => {
const { app, facilitator } = await makeApp();
const h = headAsGet((req) => app.fetch(req));
for (const [tier, amount, qs] of ROUTES) {
const r = await h(new Request(`${BASE}/v1/${tier}?${qs}`, { method: 'HEAD', headers: { 'payment-signature': 'junk', 'x-payment': 'junk' } }));
assert.equal(r.status, 402); assert.equal(decode(r.headers.get('payment-required')).accepts[0].amount, amount); assert.equal(await r.text(), '');
}
assert.equal(facilitator.verified.length, 0);
});
test('bad input: 400 before payment (never a 402, never charged)', async () => {
const { fetchApp, facilitator } = await makeApp();
for (const tier of ['lite', 'coin', 'preview']) for (const s of ['B T C', '<script>', 'A'.repeat(20), '$$']) {
const r = await fetchApp(`${BASE}/v1/${tier}?symbol=${encodeURIComponent(s)}`); assert.equal(r.status, 400, `${tier} ${s}`);
}
for (const qs of ['limit=0', 'limit=99', 'limit=x', 'min_traders=1', 'min_traders=99']) assert.equal((await fetchApp(`${BASE}/v1/scan?${qs}`)).status, 400, qs);
for (const a of ['0x12', 'vitalik.eth', `${A(1)}00`]) assert.equal((await fetchApp(`${BASE}/v1/wallet?address=${a}`)).status, 400, a);
assert.equal((await fetchApp(`${BASE}/v1/coin`, json({ symbol: 5 }))).status, 400);
assert.equal((await fetchApp(`${BASE}/v1/coin`, json({ foo: 1 }))).status, 400);
assert.equal(facilitator.verified.length, 0);
});
test('paid v2 lite + coin + scan + wallet: answers from the snapshot, settled once each at the right price, disclaimer', async () => {
const { fetchApp, facilitator } = await makeApp();
const pay = v2pay(fetchApp);
const l = await pay(`${BASE}/v1/lite?symbol=btc-perp`); assert.equal(l.status, 200); const lj = await l.json();
assert.equal(lj.tier, 'lite'); assert.equal(lj.symbol, 'BTC'); assert.equal(lj.bias, 'strong_short'); assert.equal(lj.traders_short, 4); assert.equal(lj.confidence, 'high');
assert.equal(lj.top_positions, undefined, 'positions are coin-tier'); assert.equal(lj.reliable, undefined); assert.match(lj.disclaimer, NFA); assert.ok(l.headers.get('payment-response'));
const c = await pay(`${BASE}/v1/coin`, json({ symbol: 'ETH' })); assert.equal(c.status, 200); const cj = await c.json();
assert.equal(cj.bias, 'strong_long'); assert.equal(cj.top_positions[0].address, '0x0000…0002'); assert.ok(cj.liquidation_clusters.longs.length === 5);
for (const x of [lj, cj]) assert.ok(!/0x[0-9a-fA-F]{40}/.test(JSON.stringify(x)), 'no full address on lite/coin'); assert.ok(cj.top_positions.every((p) => !('liquidation_price' in p)));
const s = await pay(`${BASE}/v1/scan?limit=3`); assert.equal(s.status, 200); const sj = await s.json();
assert.equal(sj.most_net_short[0].symbol, 'BTC'); assert.ok(sj.most_net_long.length <= 3); assert.ok(!/0x[0-9a-fA-F]{40}/.test(JSON.stringify(sj)), 'no address on scan');
const w = await pay(`${BASE}/v1/wallet?address=${A(2).toUpperCase().replace('0X', '0x')}`); assert.equal(w.status, 200); const wj = await w.json();
assert.equal(wj.address, A(2)); assert.equal(wj.in_smart_money_cohort, true); assert.match(wj.disclaimer, NFA);
assert.ok(wj.positions.every((p) => !('liquidation_price' in p) && Number.isInteger(p.liquidation_distance_pct))); assert.match(wj.data_note, /harass/);
assert.deepEqual(facilitator.settled.map((x) => x.amount), ['3000', '20000', '30000', '10000']);
});
test('FAIL-CLOSED end to end: no snapshot / stale snapshot / account state down = 503 never settled; unknown coin = 404 never settled', async () => {
const empty = await makeApp({ seed: false });
for (const [tier, , qs] of ROUTES.slice(0, 3)) {
const r = await v2pay(empty.fetchApp)(`${BASE}/v1/${tier}?${qs}`);
assert.equal(r.status, 503, tier); const j = await r.json(); assert.equal(j.charged, false); assert.ok(j.sources_failed.includes('snapshot_missing'));
}
assert.equal(empty.facilitator.settled.length, 0);
const stale = await makeApp({ store: (await seededStore({ ageMs: 40 * 60_000 })).store });
const r = await v2pay(stale.fetchApp)(`${BASE}/v1/lite?symbol=BTC`); assert.equal(r.status, 503); assert.ok((await r.json()).sources_failed.includes('snapshot_stale'));
const down = await makeApp({ net: mockHL({ fail: new Set(['state']) }), store: (await seededStore()).store });
const w = await v2pay(down.fetchApp)(`${BASE}/v1/wallet?address=${A(3)}`); assert.equal(w.status, 503);
const u = await v2pay(down.fetchApp)(`${BASE}/v1/lite?symbol=ZZZQ`); assert.equal(u.status, 404); assert.equal((await u.json()).error, 'symbol_not_listed');
assert.equal(stale.facilitator.settled.length + down.facilitator.settled.length, 0);
});
test('store: refresh under lock, lazy refresh kicked when the snapshot is missing, never overlapping', async () => {
const stub = memoryStub(); const net = mockHL();
const r1 = await refresh(stub, { fetchImpl: net.fetchImpl, now: Date.now() });
assert.equal(r1.ok, true); assert.equal(r1.covered, 10);
assert.equal((await stub.read()).snapshot.covered, 10);
await stub.lock(Date.now()); assert.equal((await refresh(stub, { fetchImpl: net.fetchImpl, now: Date.now() })).skipped, 'locked');
const st2 = memoryStub(); const kicked = []; const store = makeStore(st2, { fetchImpl: net.fetchImpl });
const d = await store.read((p) => kicked.push(p)); assert.equal(d.snapshot, null); assert.equal(kicked.length, 1);
await kicked[0]; assert.equal((await st2.read()).snapshot.covered, 10);
});
test('free preview: bias + counts only, rate limit', async () => {
let n = 0; const limiter = { limit: async () => ({ success: ++n <= 2 }) };
const { fetchApp } = await makeApp({ limiter });
const r = await fetchApp(`${BASE}/v1/preview?symbol=ETH`);
assert.equal(r.status, 200); const j = await r.json();
assert.equal(j.symbol, 'ETH'); assert.equal(j.bias, 'strong_long'); assert.equal(j.net_notional_usd, undefined); assert.equal(j.top_positions, undefined);
assert.equal(j.full.prices.coin, '$0.02'); assert.match(j.disclaimer, NFA); assert.ok(!/0x[0-9a-fA-F]{40}/.test(JSON.stringify(j)), 'no address on preview');
assert.equal((await fetchApp(`${BASE}/v1/preview?symbol=BTC`)).status, 200);
assert.equal((await fetchApp(`${BASE}/v1/preview?symbol=BTC`)).status, 429);
});
test('remote MCP: initialize, tools/list, tools/call (preview tier)', async () => {
const { fetchApp } = await makeApp();
const init = await (await mcp(fetchApp, { method: 'initialize', params: { protocolVersion: '2025-06-18' } })).json();
assert.equal(init.result.serverInfo.name, 'smartperps');
const list = await (await mcp(fetchApp, { method: 'tools/list' })).json();
assert.deepEqual(list.result.tools.map((t) => t.name), ['smart_money_bias']);
const call = await (await mcp(fetchApp, { method: 'tools/call', params: { name: 'smart_money_bias', arguments: { symbol: 'BTC' } } })).json();
assert.equal(call.result.isError, false); assert.match(call.result.content[0].text, /SmartPerps BTC: strong short/); assert.match(call.result.content[0].text, NFA);
assert.ok(!/0x[0-9a-fA-F]{40}/.test(JSON.stringify(call)), 'no address over MCP'); assert.match(init.result.instructions, /truncated/); assert.match(init.result.instructions, /harass/);
const bad = await (await mcp(fetchApp, { method: 'tools/call', params: { name: 'smart_money_bias', arguments: { symbol: '!!' } } })).json();
assert.equal(bad.result.isError, true);
});
test('discovery: info JSON, landing HTML, openapi (paid + preview only), /.well-known/x402, favicon, health', async () => {
const { fetchApp } = await makeApp();
const info = await (await fetchApp(`${BASE}/`, { headers: { accept: 'application/json' } })).json();
assert.equal(info.name, 'SmartPerps'); assert.deepEqual(info.paid.map((p) => [p.path, p.price]), [['/v1/lite', '$0.003'], ['/v1/coin', '$0.02'], ['/v1/scan', '$0.03'], ['/v1/wallet', '$0.01']]);
assert.match(info.disclaimer, NFA); assert.match(info.policy.custody, /never trades/);
const html = await (await fetchApp(`${BASE}/`, { headers: { accept: 'text/html' } })).text();
assert.match(html, /Smart<span>Perps/); assert.match(html, NFA); assert.ok(!/guarantee[ds]? (profit|return)|will go up|copy.?trad/i.test(html));
const oa = await (await fetchApp(`${BASE}/openapi.json`)).json();
assert.deepEqual(Object.keys(oa.paths).sort(), ['/v1/coin', '/v1/lite', '/v1/preview', '/v1/scan', '/v1/wallet']);
{ // docs/examples never name a real trader: only payTo, token contracts and the wallet placeholder may appear as full addresses
const allowed = new Set([PAY_TO.toLowerCase(), '0x0000000000000000000000000000000000000001']);
const txt = JSON.stringify(oa) + html + JSON.stringify(info);
for (const m of JSON.stringify(oa).matchAll(/"asset":"(0x[0-9a-fA-F]{40})"/g)) allowed.add(m[1].toLowerCase());
for (const a of txt.match(/0x[0-9a-fA-F]{40}/g) || []) assert.ok(allowed.has(a.toLowerCase()) || /^0x833589fcd6edb6e08f4c7c32d4f71b54bda02913|^0x3c499c542cef5e3811e1192ce70d8cc03d5c3359|^0xaf88d065e77c8cc2239327c5edb3a432268e5831/.test(a.toLowerCase()), `unexpected full address in docs: ${a.slice(0, 6)}…`);
assert.match(oa.paths['/v1/wallet'].get.description, /public on-chain data only/i); assert.match(oa.paths['/v1/wallet'].get.description, /harass/);
assert.match(html, /harass/); assert.match(info.policy.privacy, /0x1234…abcd/);
}
assert.equal(oa.paths['/v1/coin'].get['x-payment-info'].price.amount, '0.02'); assert.equal(oa.servers[0].url, BASE); assert.match(oa.info.description, NFA);
const wk = await (await fetchApp(`${BASE}/.well-known/x402`)).json();
assert.deepEqual(wk.resources, ['lite', 'coin', 'scan', 'wallet'].map((t) => `${BASE}/v1/${t}`)); assert.match(wk.instructions, NFA);
assert.equal((await fetchApp(`${BASE}/favicon.ico`)).headers.get('content-type'), 'image/svg+xml');
assert.equal((await fetchApp(`${BASE}/health`)).status, 200);
const hs = await (await fetchApp(`${BASE}/health?sources=1`)).json(); assert.equal(hs.sources.hyperliquid_info.status, 200); assert.equal(hs.sources.hyperliquid_leaderboard.status, 200);
});
test('stdio MCP: validates locally, uses the hosted preview, no key = free mode, scan and wallet are paid-only', async () => {
assert.deepEqual(STDIO_TOOLS.map((t) => t.name), ['smart_money_positioning', 'smart_money_scan', 'hyperliquid_wallet_profile']);
assert.equal(toSymbol({ symbol: 'eth-usd' }).symbol, 'ETH'); assert.ok(toSymbol({ symbol: '<x>' }).error); assert.ok(toSymbol({}).error); assert.ok(toAddress({ address: '0x1' }).error);
const seen = [];
const fetchImpl = async (u) => { seen.push(String(u)); return Response.json({ symbol: 'SOL', bias: 'lean_short', bias_note: 'The cohort leans short by notional.', traders_long: 2, traders_short: 5, snapshot_age_s: 40, confidence: 'high' }); };
const r = await stdioHandle({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'smart_money_positioning', arguments: { symbol: 'sol' } } }, { fetchImpl, env: {} });
assert.match(r.result.content[0].text, /SmartPerps SOL: lean short/); assert.match(r.result.content[0].text, NFA);
assert.match(STDIO_TOOLS[2].description, /harass/); assert.ok(!/liquidation prices/.test(STDIO_TOOLS[2].description));
assert.equal(seen[0], 'https://api.youraitools.co/smartperps/v1/preview?symbol=SOL');
for (const [name, args] of [['smart_money_scan', {}], ['hyperliquid_wallet_profile', { address: A(1) }]]) {
const s = await stdioHandle({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name, arguments: args } }, { fetchImpl, env: {} });
assert.equal(s.result.isError, true); assert.match(s.result.content[0].text, /paid only/);
}
});
agentgate-source: code mirror on Cloudflare Pages. All repos