import test from 'node:test';
import assert from 'node:assert/strict';
import { makeApp, BASE } from './helpers.js';
import { createFreeTier } from '../src/free-tier.js';
import { _clearCache } from '../src/engine.js';
// Shared free-first tier test (from InjectScan), parameterized for PerpCheck.
test.beforeEach(() => _clearCache());
const mk = async (appOpts = {}) => makeApp({ appOpts });
const TOOL = 'check_perp_order'; const TOOLS_EXPECTED = ['check_perp_order'];
const ARGS = { coin: 'BTC', side: 'long', size: '0.001', leverage: 10 }; const BAD_ARGS = { coin: 'BTC', side: 'sideways', size: '1' };
const PRICE = '$0.01'; const PAID = '/v1/full';
const PAYWALL_REQ = (u) => (u.includes('?') ? u : `${u}?coin=BTC&side=long&size=0.001`);
const PREVIEW_REQ = () => `${BASE}/v1/preview?coin=BTC&side=long&size=0.001`;
const FULL_CHECK = (res) => { assert.equal(res.structuredContent.verdict, 'OK'); assert.ok(res.structuredContent.liquidation && res.structuredContent.execution && res.structuredContent.funding); assert.match(res.content[0].text, /Not financial advice; the exchange decides whether an order is accepted\./); };
const IP = (n) => ({ 'cf-connecting-ip': `203.0.113.${n}` });
const rpcCall = (fetchApp, body, headers = IP(7)) => fetchApp(`${BASE}/mcp`, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body: JSON.stringify(body) }).then((r) => r.json());
const call = (fetchApp, id, headers, args = ARGS, name = TOOL) => rpcCall(fetchApp, { jsonrpc: '2.0', id, method: 'tools/call', params: { name, arguments: args } }, headers);
test('free tier: tools/list works and descriptions state the free tier', async () => {
const { fetchApp } = await mk();
const init = await rpcCall(fetchApp, { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18' } });
assert.ok(init.result.serverInfo.name);
const list = await rpcCall(fetchApp, { jsonrpc: '2.0', id: 2, method: 'tools/list' });
for (const n of TOOLS_EXPECTED) assert.ok(list.result.tools.some((t) => t.name === n), n);
assert.match(list.result.tools.find((t) => t.name === TOOL).description, /30 calls\/day/);
});
test('free tier: 30 full-result calls per IP with upgrade text, call 31 is an MCP 402 (payment_required) with the paid x402 URL', async () => {
const { fetchApp } = await mk();
for (let i = 1; i <= 30; i++) {
const r = await call(fetchApp, i, IP(7));
assert.equal(r.result.isError, false, JSON.stringify(r).slice(0, 300));
assert.equal(r.result.structuredContent.free_tier.remaining_today, 30 - i);
assert.ok(r.result.structuredContent.upgrade.startsWith(`Free tier: ${30 - i}/30 calls left today. Heavy use? Pay per call in USDC via x402 (${PRICE}/call): ${BASE}${PAID}?coin=BTC`), r.result.structuredContent.upgrade);
const lines = r.result.content[0].text.split('\n');
assert.equal(lines[lines.length - 1], r.result.structuredContent.upgrade);
if (i === 1) FULL_CHECK(r.result);
}
const over = await call(fetchApp, 31, IP(7));
assert.equal(over.result.isError, true);
assert.equal(over.result.structuredContent.error, 'payment_required');
assert.equal(over.result.structuredContent.status, 402);
assert.equal(over.result.structuredContent.reason, 'free_tier_daily_limit');
assert.equal(over.result.structuredContent.x402.price, PRICE);
assert.ok(over.result.structuredContent.x402.url.startsWith(`${BASE}${PAID}`));
assert.match(over.result.content[0].text, /30\/30/);
// The paid URL from the message answers with the normal x402 402 + PAYMENT-REQUIRED (paywall unchanged).
const paid = await fetchApp(PAYWALL_REQ(over.result.structuredContent.x402.url));
assert.equal(paid.status, 402); assert.ok(paid.headers.get('payment-required'));
// Another IP still has its own 30.
const other = await call(fetchApp, 32, IP(8));
assert.equal(other.result.isError, false); assert.equal(other.result.structuredContent.free_tier.remaining_today, 29);
});
test('free tier: invalid input does not use a free call; global daily cap falls back to paid', async () => {
const { fetchApp } = await mk({ freeTier: createFreeTier({ perClient: 30, globalDaily: 2 }) });
const bad = await call(fetchApp, 1, IP(9), BAD_ARGS);
assert.equal(bad.result.isError, true); assert.ok(!bad.result.structuredContent || bad.result.structuredContent.error !== 'payment_required');
assert.equal((await call(fetchApp, 2, IP(9))).result.structuredContent.free_tier.remaining_today, 29);
assert.equal((await call(fetchApp, 3, IP(10))).result.isError, false);
const g = await call(fetchApp, 4, IP(11));
assert.equal(g.result.structuredContent.error, 'payment_required'); assert.equal(g.result.structuredContent.reason, 'free_tier_global_cap');
});
test('free tier: IPv6 clients are counted per /64; MCP body size limit; JSON-RPC batches capped at 10', async () => {
const { fetchApp } = await mk({ freeTier: createFreeTier({ perClient: 1, globalDaily: 100 }) });
assert.equal((await call(fetchApp, 1, { 'cf-connecting-ip': '2001:db8:1:2::1' })).result.isError, false);
assert.equal((await call(fetchApp, 2, { 'cf-connecting-ip': '2001:db8:1:2:ffff::9' })).result.structuredContent.error, 'payment_required');
assert.equal((await call(fetchApp, 3, { 'cf-connecting-ip': '2001:db8:1:3::1' })).result.isError, false);
const big = await fetchApp(`${BASE}/mcp`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'ping', params: { pad: 'x'.repeat(20_000) } }) });
assert.equal(big.status, 413);
const many = await (await fetchApp(`${BASE}/mcp`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(Array.from({ length: 15 }, (_, i) => ({ jsonrpc: '2.0', id: i + 1, method: 'ping' }))) })).json();
assert.equal(many.length, 10);
});
test('free tier: unpaid paid route is still a 402 and the preview carries an upgrade field', async () => {
const { fetchApp } = await mk();
const r = await fetchApp(PAYWALL_REQ(`${BASE}${PAID}`));
assert.equal(r.status, 402); assert.ok(r.headers.get('payment-required'));
const p = await (await fetchApp(PREVIEW_REQ())).json();
assert.match(p.upgrade, /30 calls\/day/); assert.match(p.upgrade, /Pay per call in USDC via x402/);
});
test('free tier: an upstream failure is refunded and reported as UNKNOWN (never OK)', async () => {
const { mockVenues } = await import('./mock.js');
const { fetchApp } = await makeApp({ net: mockVenues({ fail: new Set(['metaAndAssetCtxs']) }), appOpts: { freeTier: createFreeTier({ perClient: 2, globalDaily: 100 }) } });
const r = await call(fetchApp, 1, IP(20));
assert.equal(r.result.isError, true); assert.equal(r.result.structuredContent.verdict, 'UNKNOWN'); assert.equal(r.result.structuredContent.confidence, 'low');
assert.ok(r.result.structuredContent.sources_failed.includes('metaAndAssetCtxs')); assert.match(r.result.content[0].text, /do not treat this as OK/);
assert.match(r.result.content[0].text, /Not financial advice; the exchange decides/);
});
agentgate-source: code mirror on Cloudflare Pages. All repos