x402-tokenverdict / tree / src/card.js · raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
// Shareable public card: GET /t/<token> (HTML) and /t/<token>/card.svg. Shows the verdict, score and a few public facts
// (free preview tier), with links to buy the full report: in the browser (any EIP-1193 wallet signs one x402 USDC
// authorization; no account, no extension-specific code) or over the API / MCP.
import { COLORS, LOGO_SVG } from './brand.js';

const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch]));
const money = (n) => (n == null ? 'n/a' : n >= 1e6 ? `$${(n / 1e6).toFixed(2)}M` : n >= 1e3 ? `$${(n / 1e3).toFixed(1)}k` : `$${Math.round(n)}`);
const ageTxt = (d) => (d == null ? 'unknown' : d < 1 ? `${Math.max(1, Math.round(d * 24))} h` : d < 60 ? `${Math.round(d)} days` : `${Math.round(d / 30.4)} months`);
const label = (p) => (p.symbol ? `${p.symbol}${p.name && p.name !== p.symbol ? ` (${p.name})` : ''}` : `${p.token.slice(0, 6)}…${p.token.slice(-4)}`);

/** Facts that are public on the card (subset of the preview). */
export function cardFacts(r) {
  const s = r.checks.sellability;
  return {
    token: r.token, name: r.name, symbol: r.symbol, verdict: r.verdict, score: r.score, confidence: r.confidence,
    sellable: s.simulated ? (s.buy_ok && s.sell_ok ? 'yes' : s.buy_ok ? 'NO (sell failed)' : 'buy failed') : 'not simulated',
    liquidity_usd: r.checks.liquidity.usd, age_days: r.checks.age?.days ?? null, verified: r.checks.contract.verified,
    flags: r.reasons.reduce((c, x) => { if (x.severity === 'danger' || x.severity === 'warn') c[x.severity]++; return c; }, { danger: 0, warn: 0 }),
    checked_at: r.checked_at,
  };
}

export function cardSvg(f) {
  const col = COLORS[f.verdict] || COLORS.UNKNOWN;
  const name = esc(label(f)).slice(0, 40);
  const pct = Math.max(0, Math.min(100, f.score));
  return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-label="TokenVerdict ${esc(f.symbol || '')} ${esc(f.verdict)} ${f.score}/100">
<rect width="1200" height="630" fill="#0b1020"/><rect width="1200" height="12" fill="${col}"/>
<g transform="translate(60,48) scale(1.25)">${LOGO_SVG.replace(/^<svg[^>]*>|<\/svg>$/g, '')}</g>
<text x="155" y="105" font-family="DejaVu Sans,Arial,sans-serif" font-size="42" font-weight="700" fill="#e6e9f0">Token<tspan fill="#38bdf8">Verdict</tspan></text>
<text x="64" y="200" font-family="DejaVu Sans,Arial,sans-serif" font-size="46" font-weight="700" fill="#e6e9f0">${name}</text>
<text x="64" y="240" font-family="DejaVu Sans Mono,monospace" font-size="22" fill="#64748b">${esc(f.token)} · Base</text>
<rect x="64" y="275" rx="24" width="${f.verdict.length * 62 + 70}" height="120" fill="${col}" fill-opacity="0.14" stroke="${col}" stroke-width="4"/>
<text x="99" y="362" font-family="DejaVu Sans,Arial,sans-serif" font-size="88" font-weight="800" fill="${col}">${esc(f.verdict)}</text>
<text x="${f.verdict.length * 62 + 170}" y="362" font-family="DejaVu Sans,Arial,sans-serif" font-size="72" font-weight="800" fill="#e6e9f0">${f.score}<tspan font-size="36" fill="#94a3b8">/100</tspan></text>
<rect x="64" y="420" width="1072" height="14" rx="7" fill="#1e293b"/><rect x="64" y="420" width="${Math.round(10.72 * pct)}" height="14" rx="7" fill="${col}"/>
<text x="64" y="492" font-family="DejaVu Sans,Arial,sans-serif" font-size="28" fill="#cbd5e1">Sellable: ${esc(f.sellable)} · Liquidity: ${esc(money(f.liquidity_usd))} · Age: ${esc(ageTxt(f.age_days))} · ${f.flags.danger} red / ${f.flags.warn} warn</text>
<text x="64" y="570" font-family="DejaVu Sans,Arial,sans-serif" font-size="22" fill="#64748b">Checked ${esc(String(f.checked_at).slice(0, 16).replace('T', ' '))} UTC · not financial advice · api.youraitools.co/tokenverdict</text>
</svg>`;
}

export function cardHtml({ origin, f, prices }) {
  const col = COLORS[f.verdict] || COLORS.UNKNOWN;
  const title = `${label(f)} on Base: ${f.verdict} (${f.score}/100) | TokenVerdict`;
  const desc = `TokenVerdict: ${f.verdict} ${f.score}/100. Sellable: ${f.sellable}. Liquidity ${money(f.liquidity_usd)}. ${f.flags.danger} red flags, ${f.flags.warn} warnings. Honeypot sell simulation, owner powers, LP and holders. Not financial advice.`;
  const url = `${origin}/t/${f.token}`;
  const share = `https://x.com/intent/post?text=${encodeURIComponent(`${f.symbol ? '$' + f.symbol : f.token} scored ${f.verdict} ${f.score}/100 on TokenVerdict (Base token safety check)`)}&url=${encodeURIComponent(url)}`;
  const apiQuick = `${origin}/v1/quick?token=${f.token}`;
  return `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(title)}</title>
<meta name="description" content="${esc(desc)}">
<link rel="canonical" href="${esc(url)}">
<meta property="og:type" content="website"><meta property="og:title" content="${esc(title)}"><meta property="og:description" content="${esc(desc)}"><meta property="og:url" content="${esc(url)}">
<meta property="og:image" content="${esc(origin)}/og/${f.verdict.toLowerCase()}.png"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="${esc(title)}"><meta name="twitter:description" content="${esc(desc)}"><meta name="twitter:image" content="${esc(origin)}/og/${f.verdict.toLowerCase()}.png">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<style>
:root{color-scheme:dark;--bg:#0b0d12;--panel:#131722;--line:#232a3a;--text:#e6e9f0;--muted:#9aa3b5;--c:${col}}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:16px/1.6 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Arial,sans-serif}
main{max-width:860px;margin:0 auto;padding:36px 20px 64px}a{color:#7c9cff;text-decoration:none}a:hover{text-decoration:underline}
.brand{display:flex;gap:10px;align-items:center;font-weight:700;font-size:1.2rem;color:var(--text)}.brand span{color:#38bdf8}
.card{background:var(--panel);border:1px solid var(--line);border-radius:18px;padding:22px;margin-top:22px}
.card img{width:100%;height:auto;border-radius:12px;display:block}
.row{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}.btn{display:inline-block;padding:10px 16px;border-radius:10px;border:1px solid var(--line);background:#0f131c;color:var(--text);cursor:pointer;font:inherit}
.btn.main{background:var(--c);color:#111;border:0;font-weight:700}.muted{color:var(--muted)}code,pre{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.86rem}
pre{white-space:pre-wrap;word-break:break-word;background:#0f131c;border:1px solid var(--line);border-radius:12px;padding:14px;margin:12px 0 0}
input{width:100%;background:#0f131c;color:var(--text);border:1px solid var(--line);border-radius:10px;padding:10px 12px;font:inherit}
ul.r{list-style:none;padding:0;margin:10px 0 0}ul.r li{padding:6px 0;border-bottom:1px solid var(--line)}.s-danger{color:#f87171}.s-warn{color:#fbbf24}.s-info{color:#9aa3b5}.s-good{color:#4ade80}
footer{margin-top:36px;color:var(--muted);font-size:.88rem}
</style></head>
<body><main>
<a class="brand" href="/">${LOGO_SVG.replace('width="64" height="64"', 'width="34" height="34"')}<div>Token<span>Verdict</span></div></a>
<div class="card">
  <img src="/t/${esc(f.token)}/card.svg" alt="${esc(`${label(f)}: ${f.verdict} ${f.score}/100`)}" width="1200" height="630">
  <p style="margin:14px 0 0"><b>${esc(label(f))}</b> <span class="muted"><code>${esc(f.token)}</code> on Base</span></p>
  <p class="muted" style="margin:4px 0 0">Verdict <b style="color:var(--c)">${esc(f.verdict)}</b>, score ${f.score}/100, confidence ${esc(f.confidence)}. ${f.flags.danger} red flag${f.flags.danger === 1 ? '' : 's'}, ${f.flags.warn} warning${f.flags.warn === 1 ? '' : 's'}. Checked ${esc(String(f.checked_at).slice(0, 16).replace('T', ' '))} UTC.</p>
  <div class="row">
    <button class="btn main" data-tier="quick" type="button">Unlock full report: ${esc(prices.quick)} USDC</button>
    <button class="btn" data-tier="lite" type="button">Reasons only: ${esc(prices.lite)}</button>
    <button class="btn" data-tier="deep" type="button">Deep report: ${esc(prices.deep)}</button>
    <a class="btn" href="${esc(share)}" target="_blank" rel="noopener">Share on X</a>
  </div>
  <p class="muted" style="font-size:.9rem;margin-top:10px">Pay with any Base wallet in your browser (one USDC signature, no account; the facilitator pays gas). Only successful reports are charged.</p>
  <div id="out"></div>
</div>
<div class="card">
  <b>Check another Base token</b>
  <form id="f" style="margin-top:10px"><input id="q" placeholder="0x... token contract address on Base" spellcheck="false" autocomplete="off"></form>
</div>
<div class="card">
  <b>For agents</b>
  <pre>curl -i "${esc(apiQuick)}"   # 402 with x402 terms; any x402 client pays it
MCP: { "mcpServers": { "tokenverdict": { "type": "http", "url": "${esc(origin)}/mcp" } } }</pre>
</div>
<footer>Automated, informational risk signals from public on-chain and market data at the time of the check. Not financial advice, not an audit, no guarantee: a token that passes can still lose value or be changed by whoever controls it. <a href="/">How it works and limits</a>.</footer>
</main>
<script>
const T = ${JSON.stringify(f.token)};
const BASE = location.pathname.split('/t/')[0];
document.getElementById('f').addEventListener('submit', (e) => { e.preventDefault(); const v = document.getElementById('q').value.trim(); if (/^0x[0-9a-fA-F]{40}$/.test(v)) location.href = BASE + '/t/' + v.toLowerCase(); else alert('Enter a 0x... Base token address'); });
const out = document.getElementById('out');
const escH = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
const b64 = (o) => btoa(unescape(encodeURIComponent(JSON.stringify(o))));
const unb64 = (s) => JSON.parse(decodeURIComponent(escape(atob(s))));
function show(html) { out.innerHTML = html; }
function render(j) {
  const rs = (j.reasons || []).map((r) => '<li class="s-' + escH(r.severity) + '">' + escH(r.severity.toUpperCase()) + ': ' + escH(r.text) + '</li>').join('');
  show('<h3 style="margin:18px 0 0">' + escH(j.verdict) + ' ' + escH(j.score) + '/100 <span class="muted">(' + escH(j.tier) + ' report)</span></h3><ul class="r">' + rs + '</ul><details style="margin-top:10px"><summary>Full JSON</summary><pre>' + escH(JSON.stringify(j, null, 2)) + '</pre></details>');
}
async function unlock(tier) {
  const eth = window.ethereum;
  const url = BASE + '/v1/' + tier + '?token=' + T;
  if (!eth) { show('<p class="muted">No browser wallet found. Open this page in a wallet browser (Coinbase Wallet, MetaMask, Rabby...) or call the API with any x402 client: <code>' + escH(location.origin + url) + '</code></p>'); return; }
  try {
    show('<p class="muted">Getting payment terms...</p>');
    const r0 = await fetch(url);
    if (r0.status !== 402) { const j = await r0.json().catch(() => ({})); show('<p class="muted">' + escH(j.message || ('Unexpected HTTP ' + r0.status)) + '</p>'); return; }
    const pr = unb64(r0.headers.get('PAYMENT-REQUIRED'));
    const req = pr.accepts.find((a) => a.network === 'eip155:8453' && a.scheme === 'exact');
    const [from] = await eth.request({ method: 'eth_requestAccounts' });
    try { await eth.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0x2105' }] }); } catch (e) { /* the wallet will show the chain in the signature request */ }
    const now = Math.floor(Date.now() / 1000);
    const nonce = '0x' + Array.from(crypto.getRandomValues(new Uint8Array(32)), (b) => b.toString(16).padStart(2, '0')).join('');
    const auth = { from, to: req.payTo, value: String(req.amount), validAfter: String(now - 600), validBefore: String(now + (req.maxTimeoutSeconds || 120)), nonce };
    const typed = { types: { EIP712Domain: [{ name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }], TransferWithAuthorization: [{ name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'validAfter', type: 'uint256' }, { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' }] }, primaryType: 'TransferWithAuthorization', domain: { name: req.extra.name, version: req.extra.version, chainId: 8453, verifyingContract: req.asset }, message: auth };
    show('<p class="muted">Sign the USDC authorization (' + (Number(req.amount) / 1e6) + ' USDC to ' + escH(req.payTo) + ') in your wallet...</p>');
    const signature = await eth.request({ method: 'eth_signTypedData_v4', params: [from, JSON.stringify(typed)] });
    show('<p class="muted">Running the check and settling...</p>');
    const r = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': b64({ x402Version: 2, resource: pr.resource, accepted: req, payload: { authorization: auth, signature }, extensions: pr.extensions || {} }) } });
    const j = await r.json().catch(() => ({}));
    if (!r.ok) { show('<p class="muted">Not charged: ' + escH(j.message || j.error || ('HTTP ' + r.status)) + '</p>'); return; }
    render(j);
  } catch (e) { show('<p class="muted">' + escH(e && e.message ? e.message : e) + '</p>'); }
}
document.querySelectorAll('button[data-tier]').forEach((b) => b.addEventListener('click', () => unlock(b.dataset.tier)));
</script>
</body></html>`;
}

export function cardErrorHtml({ message, status }) {
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>TokenVerdict: ${esc(status)}</title><meta name="robots" content="noindex"><style>body{background:#0b0d12;color:#e6e9f0;font:16px/1.6 system-ui,sans-serif;max-width:720px;margin:0 auto;padding:48px 20px}a{color:#7c9cff}</style></head><body><h1>TokenVerdict</h1><p>${esc(message)}</p><p><a href="/">Check a Base token</a></p></body></html>`;
}

agentgate-source: code mirror on Cloudflare Pages. All repos