x402-perpcheck / tree / test/free-tier.test.js ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
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