// PerpCheck: tool definition used by the generic app (routes, inputs, views, copy).
import { parseOrder, checkOrder, liteView, previewView as pv0, InputError, Incomplete, DISCLAIMER, LIMITS, ENGINE_VERSION, USER_AGENT, HL_HOSTS } from './engine.js';
import { LITE_EXAMPLE, FULL_EXAMPLE } from './examples.js';
import { withDeadline } from './deadline.js';
export { InputError, Incomplete, DISCLAIMER, LIMITS, ENGINE_VERSION, USER_AGENT };
export const NAME = 'PerpCheck';
export const SLUG = 'perpcheck';
export const SOURCE_URL = 'https://agentgate-source.pages.dev/x402-perpcheck/';
export const NPM_URL = 'https://agentgate-source.pages.dev/x402-perpcheck/'; // npm paused: self-host from source (key `npm` kept for compatibility)
export const SELF_HOST = { source: 'https://agentgate-source.pages.dev/x402-perpcheck/', git: 'https://agentgate-source.pages.dev/x402-perpcheck/x402-perpcheck.git', tarball: 'https://agentgate-source.pages.dev/x402-perpcheck/x402-perpcheck.tar.gz', run: 'npm install --omit=dev --ignore-scripts && node src/mcp-stdio.js', note: 'npm publishing is paused; run the stdio MCP server from source' };
export const SOURCES = { hyperliquid_info: HL_HOSTS[0], hyperliquid_info_fallback: HL_HOSTS[1] };
export const DESCRIPTION = 'Pre-trade order check for Hyperliquid perps, for trading agents: will this order be rejected, and what does it cost and risk? Send coin, side, size or notional, optional limit price, leverage, cross/isolated and reduce-only. Returns REJECT_LIKELY / CAUTION / OK with each exchange-rule problem and a fix value (size decimals, price tick, $10 minimum, max leverage by margin tier, isolated-only, open-interest cap, delisted, far-from-mark limits), plus isolated liquidation estimate, taker impact from the live L2 book, funding carry per day and size vs 24 h volume. Read-only: never places, signs or holds orders. Fail-closed. Paid per call over x402 or prepaid credits; free preview and free remote MCP tier. Not financial advice; the exchange decides whether an order is accepted.';
const P = {
coin: { type: 'string', description: 'Hyperliquid main-dex perp name, e.g. BTC, ETH, SOL, HYPE, kPEPE' },
side: { type: 'string', enum: ['long', 'short', 'buy', 'sell'], description: 'long/buy or short/sell' },
size: { type: 'string', description: 'Order size in coins, exactly as you would send it (decimals are checked), e.g. 0.001. Give size or notional.' },
notional: { type: 'number', description: 'Order value in USD (instead of size), e.g. 100' },
price: { type: 'string', description: 'Limit price exactly as you would send it (tick rule is checked). Omit for a market order (then taker impact is estimated from the L2 book).' },
leverage: { type: 'number', description: 'Leverage, e.g. 10 (default 1)' },
margin: { type: 'string', enum: ['cross', 'isolated'], description: 'cross (default) or isolated' },
reduceOnly: { type: 'boolean', description: 'true if the order only reduces a position (exempt from the minimum and OI cap)' },
};
const SCHEMA = { properties: P, required: ['coin', 'side'] };
const INPUT = { coin: 'BTC', side: 'long', size: '0.001', leverage: 10 };
export const ROUTES = [
{ tier: 'lite', path: '/v1/lite', key: 'priceLite', env: 'X402_PRICE_LITE', dflt: '$0.003', op: 'perp_order_check_lite',
summary: 'Hyperliquid pre-trade order check: REJECT_LIKELY / CAUTION / OK with each exchange-rule issue and a fix value',
desc: 'Pre-trade check of a Hyperliquid perp order before you send it: REJECT_LIKELY / CAUTION / OK, each exchange-rule issue with a fix value (size decimals, price tick, $10 minimum, max leverage by margin tier, isolated-only, OI cap, delisted, far-from-mark limit), warnings and the coin rules. Read-only; never places or signs orders. Fail-closed: a failed source is listed, never OK. Not financial advice; the exchange decides whether an order is accepted.',
returns: 'verdict, issues[] {code, msg, fix}, warnings[], rules (szDecimals, price decimals, min notional, max leverage at size, isolated-only), size, notional_usd, confidence, sources_failed.',
input: INPUT, schema: SCHEMA, example: LITE_EXAMPLE },
{ tier: 'full', path: '/v1/full', key: 'priceFull', env: 'X402_PRICE_FULL', dflt: '$0.01', op: 'perp_order_check_full',
summary: 'Full Hyperliquid pre-trade check: lite plus isolated liquidation estimate, taker impact from the live L2 book, funding carry per day and market facts',
desc: 'Full pre-trade check of a Hyperliquid perp order: everything in lite (verdict, rule issues with fixes) plus the isolated liquidation price and distance, taker impact and average fill from the live L2 book for market orders, funding carry per day (who pays), mark/oracle premium, open interest and size vs 24 h volume. Read-only; never places or signs orders. Fail-closed. Not financial advice; the exchange decides whether an order is accepted.',
returns: 'Lite fields plus liquidation {estimate_isolated, distance_pct}, execution {mid, spread_bps, est_avg_fill, est_impact_bps, book_depth_covered}, funding {hourly_rate, apr_pct, est_cost_per_day_usd, direction}, market {mark, oracle, premium_bps, open_interest_usd, day_volume_usd, size_vs_day_volume_pct}.',
input: INPUT, schema: SCHEMA, example: FULL_EXAMPLE },
];
export const PREVIEW = { path: '/v1/preview', tier: 'preview', input: INPUT, schema: SCHEMA, returns: 'verdict, issue and warning codes, confidence and sources_failed (no messages, fix values, liquidation, impact or funding)' };
export function parseInput(tier, raw = {}) { return parseOrder(raw); }
export function run(tier, v, ctx) { return checkOrder(v, ctx); }
export function paidView(tier, r) { return tier === 'lite' ? liteView(r) : r; }
export function previewView(r) { return pv0(r); }
export function fullLinks(origin, v, prices) {
const q = new URLSearchParams(Object.fromEntries(Object.entries({ coin: v.coin, side: v.side > 0 ? 'long' : 'short', size: v.rawSize, notional: v.rawSize === undefined ? v.notional : undefined, price: v.rawPrice, leverage: v.leverage, margin: v.margin, reduceOnly: v.reduceOnly ? 'true' : undefined }).filter(([, x]) => x !== undefined)));
return { lite: `${origin}/v1/lite?${q}`, full: `${origin}/v1/full?${q}`, prices, adds: 'issue messages and fix values plus the coin rules (lite); isolated liquidation estimate, taker impact, funding carry and market facts (full)' };
}
const TOOL_DESC = 'Checks a Hyperliquid perp order BEFORE your agent sends it: would the exchange reject it, and what does it cost and risk? Give coin, side and size (coins, as you would send it) or notional (USD); optional limit price (omit = market order), leverage, margin cross/isolated, reduceOnly. Returns verdict REJECT_LIKELY / CAUTION / OK, issues[] with fix values (size decimals, price tick, $10 minimum, max leverage by margin tier, isolated-only, OI cap, delisted, far-from-mark), warnings[], rules, isolated liquidation estimate and distance, taker impact from the live L2 book, funding carry per day, market facts, confidence and sources_failed. Read-only: never places, signs or holds orders and needs no keys.';
export const MCP_INSTRUCTIONS = 'Call check_perp_order with the Hyperliquid perp order you are about to send (coin, side, size or notional, optional price, leverage, margin, reduceOnly). Fix every issue before sending; a CAUTION with sources_failed means part of the check could not run. Read-only, no keys. Not financial advice; the exchange decides whether an order is accepted.';
export const MCP_TOOLS = [
{ name: 'check_perp_order', title: 'Hyperliquid pre-trade order check', description: TOOL_DESC, inputSchema: { type: 'object', properties: { ...P, size: { ...P.size, type: ['string', 'number'] }, price: { ...P.price, type: ['string', 'number'] } }, required: ['coin', 'side'] }, annotations: { readOnlyHint: true, openWorldHint: true, idempotentHint: false, destructiveHint: false } },
];
export function resultText(r) {
const head = `PerpCheck ${r.coin} ${r.side} ${r.order_type} ${r.size} (~$${r.notional_usd}) ${r.leverage}x ${r.margin}: ${r.verdict} (confidence ${r.confidence}${r.sources_failed?.length ? `; failed: ${r.sources_failed.join(', ')}` : ''}).`;
const iss = (r.issues || []).map((i) => `- ISSUE ${i.code}: ${i.msg}${i.fix !== undefined ? ` (fix: ${i.fix})` : ''}`);
const w = (r.warnings || []).map((x) => `- warning ${x.code}: ${x.msg}`);
const facts = [];
if (r.liquidation) facts.push(`liq est ${r.liquidation.estimate_isolated} (${r.liquidation.distance_pct}% away)`);
if (r.execution) facts.push(`impact ${r.execution.est_impact_bps} bps, spread ${r.execution.spread_bps} bps`);
if (r.funding) facts.push(`funding ${r.funding.apr_pct}% APR, ${r.funding.direction} ~$${Math.abs(r.funding.est_cost_per_day_usd)}/day`);
return [head, ...iss, ...w, facts.length ? facts.join('; ') : '', DISCLAIMER].filter(Boolean).join('\n');
}
export function previewText(pv, origin, v, prices) {
return `PerpCheck ${pv.coin} ${pv.side}: ${pv.verdict}; issues: ${pv.issue_codes.join(', ') || 'none'}; warnings: ${pv.warning_codes.join(', ') || 'none'}; confidence ${pv.confidence}.\nFix values and rules: /v1/lite (${prices.lite}); liquidation, impact and funding: /v1/full (${prices.full}). x402 USDC on Base, Polygon, Arbitrum One or Solana.\n${DISCLAIMER}`;
}
export async function probeSources({ fetchImpl }) {
const probe = async (name, url) => { const t0 = Date.now(); try { const r = await withDeadline(fetchImpl(url, { method: 'POST', headers: { 'content-type': 'application/json', 'user-agent': USER_AGENT }, body: '{"type":"perpsAtOpenInterestCap"}' }), 5000, name); try { await r.body?.cancel(); } catch { /* ignore */ } return [name, { status: r.status, ms: Date.now() - t0 }]; } catch (e) { return [name, { error: String(e?.message || e).slice(0, 80), ms: Date.now() - t0 }]; } };
return Object.fromEntries(await Promise.all([probe('hyperliquid_info', HL_HOSTS[0]), probe('hyperliquid_info_fallback', HL_HOSTS[1])]));
}
export const LANDING = {
title: 'PerpCheck: Hyperliquid pre-trade order check for trading agents (rejects, liquidation, impact, funding; x402 API + MCP)',
h1: 'Perp<span>Check</span>',
lead: 'Will this Hyperliquid order be rejected, and what does it cost and risk? Send the perp order your agent is about to place and PerpCheck answers <b>REJECT_LIKELY / CAUTION / OK</b> with every exchange-rule problem and a fix value: size decimals, the price tick rule, the $10 minimum, max leverage by margin tier, isolated-only coins, the open-interest cap, delisted coins and limits far from mark. The full tier adds the isolated liquidation estimate, taker impact from the live L2 book, funding carry per day and size vs 24 h volume. <b>Read-only</b>: PerpCheck never places, signs or holds orders and never asks for keys. Fail-closed: if a public source fails, the answer is never OK.',
snippet: (origin) => `// Trading agent: check the order before sending it to Hyperliquid
const r = await (await pay('${origin}/v1/lite?coin=ETH&side=long&size=0.0123456&price=3000.123&leverage=60')).json();
if (r.verdict === 'REJECT_LIKELY') console.log(r.issues.map((i) => [i.code, i.fix]));
const full = await (await pay('${origin}/v1/full?coin=BTC&side=long&size=0.001&leverage=10')).json();
console.log(full.liquidation.distance_pct, full.execution.est_impact_bps, full.funding.est_cost_per_day_usd);`,
};
agentgate-source: code mirror on Cloudflare Pages. All repos