AgentLens — Source

Complete source of the AgentLens trust & price oracle: indexer, scoring engine, x402 API, MCP server, Cloudflare Worker, and the OKX.AI ASP registration tooling.

↓ 下载 source.tar.gz ← 回到看板 API /market

README.md 141 行

# AgentLens

**A trust and price oracle for the OKX.AI agent economy.**

Buyer agents can already pay each other on OKX.AI — settlement works, escrow works, x402 works. What they
cannot do is decide *who to pay*. Ratings are thin and self-reported, prices are arbitrary, and a listed
service looks identical whether it has delivered 3,000 orders or none at all.

AgentLens turns the live marketplace into machine-readable answers to two questions:

1. **Is this seller reliable?**
2. **Is this price normal?**

- Dashboard: https://agentlens.am518.uk
- Source: https://github.com/am5188/agentlens
- API base: https://agentlens.am518.uk
- OKX.AI listing: ASP **#13437** — two services:
  - *Agent Trust & Price Oracle* — A2MCP, 0.01 USDT per call, endpoint `/recommend`
  - *Onchain Research Report* — A2A, 0.5 USDT, task-matchable (the platform routes paid tasks to online ASPs)

---

## What it measures (live data, 2026-09-09)

| Metric | Value |
|---|---|
| Agents indexed | **497** (437 online) |
| Services | **1,072** (867 per-call · 68 subscription · 137 free) |
| Deliveries | **14,129** |
| Reviews | **3,975** (110 agents rated) |
| Agents with zero deliveries | **131** — 26% of supply is unproven |
| Per-call price | median **0.05 USDT**, p25 0.01, p75 0.30, max **5,288** |
| Subscription price | median **10 USDT/month**, p25 3, p75 10, max 680 |

The spread between the median and the maximum per-call price is five orders of magnitude. That is not a
market with a pricing problem — it is a market with no pricing *signal*.

## How the trust score works

```
trust = 0.34 · BayesianShrunkRating   (shrunk toward the market mean, prior strength C = 10 reviews)
      + 0.26 · log1p(deliveries) / log1p(max deliveries)
      + 0.18 · approval rate
      + 0.12 · online status
      + 0.10 · freshness (updated within 90 days)
```

The shrinkage is the point: a 5.0 average from one review cannot outrank a 4.9 from three thousand. Every
record carries an explicit `confidence` label (`none` / `low` / `medium` / `high`) so a caller knows how much
weight the score deserves.

## API

### Free

| Endpoint | Returns |
|---|---|
| `GET /health` | Service status, indexed counts, data timestamp |
| `GET /market` | Market-wide stats, histograms, price benchmarks |
| `GET /price?category=` | Fair-price percentiles for a category |
| `GET /agents?category=&limit=` | Ranked supply list by trust score |

### Paid (x402, USDT on X Layer)

| Endpoint | Price | Returns |
|---|---|---|
| `GET /trust/:agentId` | 0.002 USDT | Full trust record + every service + review distribution |
| `GET /compare?a=&b=` | 0.005 USDT | Head-to-head on trust, rating, deliveries, price verdict |
| `POST /recommend` | 0.010 USDT | Task → ranked shortlist with reasons + benchmark |

**Payment flow**

```bash
# 1. unpaid call → HTTP 402
curl -i https://agentlens.am518.uk/trust/8136
# { "x402Version":1, "accepts":[{ "scheme":"exact", "network":"eip155:196",
#   "asset":"0x779ded0c9e1022225f8e0630b35a9b54be713736", "maxAmountRequired":"2000",
#   "payTo":"0xab098996073a5bb301b422e8276bf68d962b8ae9" }] }

# 2. pay the exact USDT amount on X Layer, then retry with the settlement tx hash
curl -H "X-PAYMENT: 0x<txHash>" https://agentlens.am518.uk/trust/8136
```

The server verifies every payment on-chain: it reads the X Layer receipt, checks for a USDT `Transfer` log
addressed to `payTo` with an amount at least the required value, and rejects stale payments. No accounts,
no API keys, no subscriptions — an agent pays per question.

**Verification evidence** (`agentlens/test-x402.mjs`, real X Layer data):

| Case | Result |
|---|---|
| Real USDT transfer + matching `payTo` | **HTTP 200** — returned `1M · 斯巴达 trust=84.5` |
| Real transfer + wrong `payTo` | 402 `paid 0 < required 2000` |
| Fabricated tx hash | 402 `tx not found` |

The payment rail is live and proven: a USDT transfer on X Layer to `payTo` unlocks the endpoint.

## MCP server

Any MCP-capable agent (Claude Code, Codex, Hermes, OpenClaw) can call AgentLens as a tool. No npm
account or install step is needed — run it straight from this repository:

```bash
claude mcp add agentlens -- npx -y github:am5188/agentlens
```

Or point any MCP client at `command: npx`, `args: ["-y", "github:am5188/agentlens"]`.

Tools: `market_overview`, `price_benchmark`, `rank_agents` (free) and `trust_lookup`, `compare_agents`,
`recommend_agent` (x402). Set `AGENTLENS_PAYMENT=<txHash>` after settling a payment.

## Layout

```
agentlens/
  crawler.mjs        index the live marketplace (sitemap + server-rendered JSON), resumable
  score.mjs          trust scoring + price benchmarks → data/index.json
  server.mjs         local API + dashboard (dev)
  mcp.mjs            MCP stdio server
  build-worker.mjs   generate the Cloudflare Worker bundle
  public/index.html  dashboard
  worker/            deployed Worker (API + dashboard + x402)
  data/              agents.ndjson · index.json
```

## Refresh

```powershell
pwsh .\agentlens\refresh.ps1      # crawl → score → build → deploy
```

## Data source

Public OKX.AI Agent Plaza pages and their server-rendered state (`__app_data_for_ssr__`). Agent identities
are also anchored on X Layer via the OKX.AI agent registry contract
`0x8004a169fb4a3325136eb29fa0ceb6d2e539a432`.

---

Built for **OKX Dev Day 2026** — track: *OKX AI: Agents and AI-native businesses*.

crawler.mjs 225 行

// crawler.mjs — OKX.AI Agent 广场索引器
//
// 数据源:
//   1) https://www.okx.ai/sitemap/agents/{1..N}   → 全部 Agent ID
//   2) https://www.okx.ai/zh-hans/agents/{id}     → 内嵌 JSON (__app_data_for_ssr__)
//
// 输出: data/agents.ndjson (每行一个 Agent 的完整快照)
//
// 用法:
//   node agentlens/crawler.mjs probe            探测单个页面的 JSON 结构
//   node agentlens/crawler.mjs ids              只收集 Agent ID 清单
//   node agentlens/crawler.mjs crawl [--limit N] 全量抓取

import fs from "node:fs";
import path from "node:path";

const BASE = "https://www.okx.ai";
const UA =
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36";
const OUT_DIR = path.join(process.cwd(), "agentlens", "data");
const AGENTS_NDJSON = path.join(OUT_DIR, "agents.ndjson");
const IDS_JSON = path.join(OUT_DIR, "agent-ids.json");
const MAX_SITEMAP_PAGES = 40;
const CONCURRENCY = 2;

fs.mkdirSync(OUT_DIR, { recursive: true });

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function fetchText(url, tries = 3) {
  for (let i = 1; i <= tries; i++) {
    try {
      const res = await fetch(url, {
        headers: {
          "User-Agent": UA,
          Accept: "text/html,application/xhtml+xml",
          "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        },
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.text();
    } catch (e) {
      if (i === tries) throw e;
      await sleep(800 * i);
    }
  }
}

function extractAppState(html) {
  const m = html.match(
    /<script[^>]*data-id="__app_data_for_ssr__"[^>]*>([\s\S]*?)<\/script>/
  );
  if (!m) return null;
  try {
    return JSON.parse(m[1]);
  } catch (e) {
    return { __parseError: String(e) };
  }
}

function findAgentDetail(obj) {
  // 递归找到含 agentId 的 overview 对象
  const seen = new Set();
  const stack = [obj];
  while (stack.length) {
    const cur = stack.pop();
    if (!cur || typeof cur !== "object" || seen.has(cur)) continue;
    seen.add(cur);
    if (cur.overview && cur.overview.agentId) return cur;
    for (const k of Object.keys(cur)) stack.push(cur[k]);
  }
  return null;
}

function normalize(detail, agentId) {
  const o = detail.overview || {};
  const svcList = (detail.services && detail.services.list) || [];
  const reviewBlock = detail.review || detail.reviews || detail.evaluation || null;
  return {
    agentId: String(o.agentId || agentId),
    name: o.name || null,
    onlineStatus: o.onlineStatus ?? null,
    score: o.score != null ? Number(o.score) : null,
    approvalRate: o.approvalRate || null,
    usageCount: o.usageCount != null ? Number(o.usageCount) : null,
    network: o.network || null,
    chainIndex: o.chainIndex ?? null,
    ownerAddress: o.ownerAddress || null,
    registryContract: o.registryContract || null,
    registryTx: o.registryTx || null,
    createdAt: o.createdAt ?? null,
    updatedAt: o.updatedAt ?? null,
    categories: o.categories || [],
    serviceLowestFee: o.serviceLowestFee != null ? Number(o.serviceLowestFee) : null,
    description: o.description || null,
    services: svcList.map((s) => ({
      serviceId: s.serviceId ?? null,
      name: s.name ?? null,
      price: s.price != null ? Number(s.price) : null,
      priceInterval: s.priceInterval || null,
      serviceType: s.serviceType || null,
      symbol: s.symbol || null,
      freeTrial: s.freeTrial != null ? Number(s.freeTrial) : null,
      subscriptionType: s.subscriptionType ?? null,
      salesCount: s.salesCount != null ? Number(s.salesCount) : null,
      descLen: (s.description || "").length,
    })),
    serviceTotal: (detail.services && detail.services.total) ?? svcList.length,
    reviewSummary: reviewBlock
      ? {
          totalScore: reviewBlock.totalScore != null ? Number(reviewBlock.totalScore) : null,
          totalCount: reviewBlock.totalCount != null ? Number(reviewBlock.totalCount) : null,
          distribution: reviewBlock.distribution || null,
        }
      : null,
    reviews: reviewBlock && Array.isArray(reviewBlock.list)
      ? reviewBlock.list.slice(0, 20).map((r) => ({
          rating: r.rating ?? r.score ?? null,
          time: r.createdAt ?? r.time ?? null,
          textLen: (r.content || r.text || "").length,
        }))
      : null,
    reviewKeys: reviewBlock ? Object.keys(reviewBlock) : null,
    allKeys: Object.keys(detail),
    crawledAt: Date.now(),
  };
}

async function collectIds() {
  const ids = new Set();
  for (let p = 1; p <= MAX_SITEMAP_PAGES; p++) {
    let html;
    try {
      html = await fetchText(`${BASE}/sitemap/agents/${p}`);
    } catch (e) {
      console.log(`sitemap ${p}: ${e.message}`);
      break;
    }
    const before = ids.size;
    for (const m of html.matchAll(/href="\/(?:zh-hans\/)?agents\/(\d+)"/g)) ids.add(m[1]);
    console.log(`sitemap ${p}: +${ids.size - before} (total ${ids.size})`);
    if (ids.size === before) break;
    await sleep(300);
  }
  const arr = [...ids].sort((a, b) => Number(a) - Number(b));
  fs.writeFileSync(IDS_JSON, JSON.stringify(arr, null, 1));
  console.log(`共收集 ${arr.length} 个 Agent ID → ${IDS_JSON}`);
  return arr;
}

async function crawl(limit) {
  let ids;
  if (fs.existsSync(IDS_JSON)) ids = JSON.parse(fs.readFileSync(IDS_JSON, "utf8"));
  else ids = await collectIds();

  // 断点续抓:跳过已成功写入的 ID,追加写入
  const doneIds = new Set();
  if (fs.existsSync(AGENTS_NDJSON)) {
    for (const l of fs.readFileSync(AGENTS_NDJSON, "utf8").split("\n").filter(Boolean)) {
      try {
        doneIds.add(String(JSON.parse(l).agentId));
      } catch {}
    }
  }
  let todo = ids.filter((id) => !doneIds.has(String(id)));
  console.log(`已完成 ${doneIds.size},待抓取 ${todo.length}`);
  if (limit) todo = todo.slice(0, limit);

  const out = fs.createWriteStream(AGENTS_NDJSON, { flags: "a" });
  let done = 0;
  let failed = 0;
  let idx = 0;

  async function worker() {
    while (idx < todo.length) {
      const id = todo[idx++];
      try {
        const html = await fetchText(`${BASE}/zh-hans/agents/${id}`);
        const state = extractAppState(html);
        const detail = state ? findAgentDetail(state) : null;
        if (!detail) throw new Error("no appState/AgentDetail");
        out.write(JSON.stringify(normalize(detail, id)) + "\n");
      } catch (e) {
        failed++;
        console.log(`  agent ${id} FAILED: ${e.message}`);
      }
      done++;
      if (done % 25 === 0) console.log(`进度 ${done}/${todo.length} (失败 ${failed})`);
      await sleep(400);
    }
  }

  await Promise.all(Array.from({ length: CONCURRENCY }, worker));
  out.end();
  console.log(`\n完成: ${done} 抓取, ${failed} 失败 → ${AGENTS_NDJSON}`);
}

const cmd = process.argv[2] || "crawl";
if (cmd === "probe") {
  const id = process.argv[3] || "8136";
  const html = await fetchText(`${BASE}/zh-hans/agents/${id}`);
  const state = extractAppState(html);
  if (!state) {
    console.log("未找到 __app_data_for_ssr__");
    process.exit(1);
  }
  const detail = findAgentDetail(state);
  console.log("AgentDetail keys:", detail ? Object.keys(detail) : null);
  if (detail) {
    console.log("\noverview:", JSON.stringify(detail.overview, null, 1).slice(0, 2000));
    console.log("\nservices:", JSON.stringify(detail.services, null, 1).slice(0, 1500));
    for (const k of Object.keys(detail)) {
      if (k === "overview" || k === "services") continue;
      console.log(`\n${k}:`, JSON.stringify(detail[k], null, 1).slice(0, 1200));
    }
  }
} else if (cmd === "ids") {
  await collectIds();
} else if (cmd === "crawl") {
  const li = process.argv.indexOf("--limit");
  await crawl(li > 0 ? Number(process.argv[li + 1]) : 0);
} else {
  console.log("用法: probe [id] | ids | crawl [--limit N]");
}

score.mjs 287 行

// score.mjs — AgentLens 信任评分 + 定价基准引擎
//
// 输入: data/agents.ndjson
// 输出: data/index.json  (市场统计 + 每个 ASP 的信任分/定价分位 + 推荐)
//
// 用法: node agentlens/score.mjs

import fs from "node:fs";
import path from "node:path";

const DATA = path.join(process.cwd(), "agentlens", "data");
const AGENTS = path.join(DATA, "agents.ndjson");
const OUT = path.join(DATA, "index.json");

// ---------- 读取 ----------
const agents = fs
  .readFileSync(AGENTS, "utf8")
  .split("\n")
  .filter(Boolean)
  .map((l) => JSON.parse(l));

// ---------- 工具 ----------
const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
const num = (x) => (typeof x === "number" && Number.isFinite(x) ? x : null);

// 价格口径:
//   priceInterval === "month"  → 订阅制,月度价 = price
//   priceInterval === null      → 按次计价(A2MCP / 单次交付),单价 = price
const MONTHLY = { month: 1, day: 30, week: 4.345, year: 1 / 12 };
function monthlyPrice(price, interval) {
  if (price == null) return null;
  if (interval == null) return null; // 按次,不计入月度价
  const f = MONTHLY[interval];
  if (f == null) return null;
  return price * f;
}
const isPerCall = (s) => s.price != null && s.price > 0 && s.priceInterval == null;
const isSubscription = (s) => s.price != null && s.price > 0 && s.priceInterval === "month";

function quantile(sorted, q) {
  if (!sorted.length) return null;
  const pos = (sorted.length - 1) * q;
  const lo = Math.floor(pos);
  const hi = Math.ceil(pos);
  if (lo === hi) return sorted[lo];
  return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
}

// ---------- 市场统计 ----------
const services = [];
for (const a of agents) {
  for (const s of a.services || []) {
    services.push({
      agentId: a.agentId,
      agentName: a.name,
      category: (a.categories || [])[0] || "OTHER",
      serviceId: s.serviceId,
      serviceName: s.name,
      price: s.price,
      priceInterval: s.priceInterval,
      perCall: isPerCall(s) ? s.price : null,
      monthly: monthlyPrice(s.price, s.priceInterval),
      salesCount: s.salesCount,
      freeTrial: s.freeTrial,
      serviceType: s.serviceType,
      symbol: s.symbol,
      agentScore: a.score,
      agentUsage: a.usageCount,
    });
  }
}

const byCategory = {};
for (const s of services) (byCategory[s.category] ||= []).push(s);

function bench(list) {
  const sorted = list.filter((x) => x != null && x > 0).sort((a, b) => a - b);
  return {
    n: sorted.length,
    p10: quantile(sorted, 0.1),
    p25: quantile(sorted, 0.25),
    median: quantile(sorted, 0.5),
    p75: quantile(sorted, 0.75),
    p90: quantile(sorted, 0.9),
    max: sorted.length ? sorted[sorted.length - 1] : null,
  };
}

const priceBench = {};
for (const [cat, list] of Object.entries(byCategory)) {
  priceBench[cat] = {
    services: list.length,
    perCall: bench(list.map((s) => s.perCall)),
    monthly: bench(list.map((s) => s.monthly)),
  };
}

const marketPriceBench = {
  perCall: bench(services.map((s) => s.perCall)),
  monthly: bench(services.map((s) => s.monthly)),
};

function verdict(v, b) {
  if (v == null || !b || b.median == null) return "unknown";
  if (v <= b.p25) return "cheap";
  if (v <= b.p75) return "fair";
  if (v <= b.p90) return "pricey";
  return "very_pricey";
}

// ---------- 信任评分 ----------
// 设计原则:小样本用贝叶斯收缩,向全市场均值靠拢;销量、好评率、在线、活跃度加权。
const C = 10; // 先验强度(等效于 10 条评价)
const globalMean = (() => {
  const rated = agents.filter((a) => num(a.score) != null);
  if (!rated.length) return 4.0;
  return rated.reduce((s, a) => s + a.score, 0) / rated.length;
})();

const maxUsage = Math.max(1, ...agents.map((a) => num(a.usageCount) || 0));
const now = Date.now();

function trustScore(a) {
  const score = num(a.score);
  const n = num(a.reviewSummary?.totalCount) ?? 0;
  // 1) 贝叶斯收缩评分(0-5)
  const shrunk = score == null ? globalMean : (C * globalMean + score * n) / (C + n);
  const ratingPart = clamp(shrunk / 5, 0, 1);

  // 2) 交付量(对数归一)
  const usage = num(a.usageCount) || 0;
  const deliveryPart = clamp(Math.log1p(usage) / Math.log1p(maxUsage), 0, 1);

  // 3) 好评率
  const ap = a.approvalRate ? parseFloat(String(a.approvalRate)) / 100 : null;
  const approvalPart = ap == null ? 0.5 : clamp(ap, 0, 1);

  // 4) 在线状态
  const onlinePart = a.onlineStatus === 1 ? 1 : 0;

  // 5) 新鲜度(最近 90 天内更新过 = 1)
  const updated = num(a.updatedAt);
  const freshPart = updated == null ? 0.5 : clamp(1 - (now - updated) / (90 * 864e5), 0, 1);

  const raw =
    0.34 * ratingPart + 0.26 * deliveryPart + 0.18 * approvalPart + 0.12 * onlinePart + 0.1 * freshPart;

  return {
    trust: Math.round(raw * 1000) / 10, // 0-100
    parts: {
      rating: Math.round(ratingPart * 1000) / 1000,
      delivery: Math.round(deliveryPart * 1000) / 1000,
      approval: Math.round(approvalPart * 1000) / 1000,
      online: onlinePart,
      freshness: Math.round(freshPart * 1000) / 1000,
    },
    shrunkRating: Math.round(shrunk * 100) / 100,
    reviewCount: n,
    confidence:
      n >= 20 ? "high" : n >= 5 ? "medium" : n >= 1 ? "low" : "none",
  };
}

const scored = agents
  .map((a) => {
    const t = trustScore(a);
    const cat = (a.categories || [])[0] || "OTHER";
    const perCalls = (a.services || []).filter(isPerCall).map((s) => s.price);
    const monthlies = (a.services || [])
      .map((s) => monthlyPrice(s.price, s.priceInterval))
      .filter((x) => x != null);
    const myPerCall = perCalls.length ? Math.min(...perCalls) : null;
    const myMonthly = monthlies.length ? Math.min(...monthlies) : null;
    const benchCat = priceBench[cat] || marketPriceBench;
    return {
      agentId: a.agentId,
      name: a.name,
      category: cat,
      score: num(a.score),
      approvalRate: a.approvalRate,
      usageCount: num(a.usageCount) || 0,
      onlineStatus: a.onlineStatus,
      serviceCount: a.serviceTotal || (a.services || []).length,
      lowestPerCallPrice: myPerCall,
      lowestMonthlyPrice: myMonthly,
      priceVerdictPerCall: verdict(myPerCall, benchCat.perCall),
      priceVerdictMonthly: verdict(myMonthly, benchCat.monthly),
      ownerAddress: a.ownerAddress,
      registryContract: a.registryContract,
      createdAt: a.createdAt,
      updatedAt: a.updatedAt,
      ...t,
    };
  })
  .sort((x, y) => y.trust - x.trust);

// ---------- 汇总 ----------
function hist(values, edges) {
  const out = edges.map((e) => ({ upTo: e, count: 0 }));
  for (const v of values) {
    let i = edges.findIndex((e) => v <= e);
    if (i < 0) i = edges.length - 1;
    out[i].count++;
  }
  return out;
}
const perCallValues = services.map((s) => s.perCall).filter((x) => x != null && x > 0);
const monthlyValues = services.map((s) => s.monthly).filter((x) => x != null && x > 0);
const deliveryValues = agents.map((a) => num(a.usageCount) || 0);
const BIG = 1e12;

const summary = {
  generatedAt: new Date().toISOString(),
  agentCount: agents.length,
  serviceCount: services.length,
  perCallServiceCount: services.filter((s) => s.perCall != null).length,
  subscriptionServiceCount: services.filter((s) => s.monthly != null).length,
  freeServiceCount: services.filter((s) => !s.price).length,
  totalDeliveries: agents.reduce((s, a) => s + (num(a.usageCount) || 0), 0),
  onlineAgents: agents.filter((a) => a.onlineStatus === 1).length,
  agentsWithReviews: agents.filter((a) => (a.reviewSummary?.totalCount || 0) > 0).length,
  agentsWithZeroDeliveries: agents.filter((a) => !(num(a.usageCount) > 0)).length,
  totalReviews: agents.reduce((s, a) => s + (num(a.reviewSummary?.totalCount) || 0), 0),
  categoryCount: Object.keys(byCategory).length,
  categories: Object.fromEntries(
    Object.entries(byCategory)
      .map(([k, v]) => [
        k,
        {
          agents: new Set(v.map((s) => s.agentId)).size,
          services: v.length,
          deliveries: [...new Set(v.map((s) => s.agentId))].reduce(
            (s, id) => s + (num(agents.find((a) => a.agentId === id)?.usageCount) || 0),
            0
          ),
        },
      ])
      .sort((a, b) => b[1].services - a[1].services)
  ),
  marketPriceBench,
  priceBenchByCategory: priceBench,
  histograms: {
    perCall: hist(perCallValues, [0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10, 100, 1000, BIG]),
    monthly: hist(monthlyValues, [1, 3, 5, 10, 20, 50, 100, 500, BIG]),
    deliveries: hist(deliveryValues, [0, 1, 5, 10, 50, 100, 500, 1000, 10000, BIG]),
  },
  globalMeanRating: Math.round(globalMean * 100) / 100,
  trustTop20: scored.slice(0, 20).map((s) => ({
    agentId: s.agentId,
    name: s.name,
    trust: s.trust,
    score: s.score,
    reviews: s.reviewCount,
    deliveries: s.usageCount,
    category: s.category,
  })),
};

fs.writeFileSync(OUT, JSON.stringify({ summary, agents: scored }, null, 1));
console.log("=== AgentLens 市场概览 ===");
console.log(`Agent 总数:        ${summary.agentCount} (在线 ${summary.onlineAgents})`);
console.log(
  `服务总数:          ${summary.serviceCount} (按次 ${summary.perCallServiceCount} / 订阅 ${summary.subscriptionServiceCount} / 免费 ${summary.freeServiceCount})`
);
console.log(`累计交付量:        ${summary.totalDeliveries}`);
console.log(`评价总数:          ${summary.totalReviews} (有评价的 Agent ${summary.agentsWithReviews})`);
console.log(`零交付 Agent:      ${summary.agentsWithZeroDeliveries} / ${summary.agentCount}`);
const pb = marketPriceBench.perCall;
const mb = marketPriceBench.monthly;
console.log(
  `按次价中位数:      ${pb.median?.toFixed(4)} USDT  (p10 ${pb.p10?.toFixed(4)} / p25 ${pb.p25?.toFixed(4)} / p75 ${pb.p75?.toFixed(4)} / max ${pb.max?.toFixed(2)}, n=${pb.n})`
);
console.log(
  `订阅价中位数:      ${mb.median?.toFixed(2)} USDT/月  (p25 ${mb.p25?.toFixed(2)} / p75 ${mb.p75?.toFixed(2)} / max ${mb.max?.toFixed(2)}, n=${mb.n})`
);
console.log(`\n品类分布:`);
for (const [k, v] of Object.entries(summary.categories))
  console.log(
    `  ${k.padEnd(18)} agents=${String(v.agents).padStart(4)} services=${String(v.services).padStart(4)} deliveries=${v.deliveries}`
  );
console.log(`\n信任分 Top 10:`);
for (const t of summary.trustTop20.slice(0, 10))
  console.log(
    `  ${String(t.trust).padStart(5)}  ${String(t.name).slice(0, 26).padEnd(28)} 评分${t.score ?? "-"} 评价${t.reviews} 交付${t.deliveries}`
  );
console.log(`\n→ ${OUT}`);

server.mjs 318 行

// server.mjs — AgentLens API:Agent 商业的信任与定价预言机
//
// 免费层:  GET /health  /market  /price  /agents
// 付费层:  GET /trust/:id  /compare  POST /recommend   (x402,USDT on X Layer)
//
// x402 流程(HTTP 402):
//   1) 无支付凭证 → 402 + accepts[](含 payTo / asset / maxAmountRequired / network)
//   2) 客户端付款后重试,带 X-PAYMENT: <txHash>
//   3) 服务端用 X Layer RPC 校验:tx 成功 + 向 payTo 转账 USDT ≥ 要求金额 + 时间窗口内
//
// 用法: node agentlens/server.mjs  [PORT=8788] [PAY_TO=0x...]

import http from "node:http";
import fs from "node:fs";
import path from "node:path";

const DATA = path.join(process.cwd(), "agentlens", "data");
const PORT = Number(process.env.PORT || 8788);
const PAY_TO = process.env.PAY_TO || "0x0000000000000000000000000000000000000000";
const XLAYER_RPC = process.env.XLAYER_RPC || "https://rpc.xlayer.tech";
const USDT_XLAYER = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
const NETWORK = "eip155:196"; // X Layer

const index = JSON.parse(fs.readFileSync(path.join(DATA, "index.json"), "utf8"));
const byId = new Map(index.agents.map((a) => [String(a.agentId), a]));
const rawById = new Map(
  fs
    .readFileSync(path.join(DATA, "agents.ndjson"), "utf8")
    .split("\n")
    .filter(Boolean)
    .map((l) => JSON.parse(l))
    .map((a) => [String(a.agentId), a])
);

// ---------- 价格表(每次调用,单位 USDT 的最小单位 6 位小数) ----------
const PRICES = {
  "GET /trust": "2000", // 0.002 USDT
  "GET /compare": "5000", // 0.005 USDT
  "POST /recommend": "10000", // 0.01 USDT
};

// ---------- x402 校验 ----------
async function rpc(method, params) {
  const res = await fetch(XLAYER_RPC, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
  });
  const j = await res.json();
  if (j.error) throw new Error(j.error.message);
  return j.result;
}

const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";

async function verifyX402(txHash, amountRequired, payTo) {
  if (!/^0x[0-9a-fA-F]{64}$/.test(txHash)) return { ok: false, reason: "bad tx hash" };
  if (payTo === "0x" + "0".repeat(40)) return { ok: false, reason: "server payTo not configured" };
  const [tx, receipt] = await Promise.all([
    rpc("eth_getTransactionByHash", [txHash]),
    rpc("eth_getTransactionReceipt", [txHash]),
  ]);
  if (!tx) return { ok: false, reason: "tx not found" };
  if (!receipt || receipt.status !== "0x1") return { ok: false, reason: "tx not successful" };

  // 检查 receipt logs 里是否有 USDT Transfer 到 payTo 且金额足够
  const wantTo = payTo.toLowerCase().replace(/^0x/, "").padStart(64, "0");
  let paid = 0n;
  for (const lg of receipt.logs || []) {
    if ((lg.address || "").toLowerCase() !== USDT_XLAYER) continue;
    if ((lg.topics || [])[0] !== TRANSFER_TOPIC) continue;
    const to = (lg.topics || [])[2] || "";
    if (to.toLowerCase() !== "0x" + wantTo) continue;
    paid += BigInt(lg.data === "0x" ? "0x0" : lg.data);
  }
  if (paid < BigInt(amountRequired)) {
    return { ok: false, reason: `paid ${paid} < required ${amountRequired}` };
  }
  const block = await rpc("eth_getBlockByNumber", [receipt.blockNumber, false]);
  const ts = block ? Number(BigInt(block.timestamp)) * 1000 : 0;
  if (ts && Date.now() - ts > 30 * 60 * 1000) return { ok: false, reason: "payment too old" };
  return { ok: true, paid: paid.toString(), ts };
}

// ---------- 推荐算法 ----------
function recommend({ category, maxMonthlyPrice, minTrust, requireOnline, limit = 5 }) {
  let list = index.agents;
  if (category) list = list.filter((a) => a.category === category);
  if (minTrust != null) list = list.filter((a) => a.trust >= minTrust);
  if (requireOnline) list = list.filter((a) => a.onlineStatus === 1);
  if (maxMonthlyPrice != null)
    list = list.filter((a) => a.lowestMonthlyPrice == null || a.lowestMonthlyPrice <= maxMonthlyPrice);

  return list
    .map((a) => ({
      agentId: a.agentId,
      name: a.name,
      category: a.category,
      trust: a.trust,
      score: a.score,
      reviewCount: a.reviewCount,
      confidence: a.confidence,
      usageCount: a.usageCount,
      lowestPerCallPrice: a.lowestPerCallPrice,
      lowestMonthlyPrice: a.lowestMonthlyPrice,
      priceVerdict: a.priceVerdictPerCall,
      why: `${a.confidence} confidence (${a.reviewCount} reviews), ${a.usageCount} deliveries, price=${a.priceVerdictPerCall}`,
    }))
    .sort((x, y) => y.trust - x.trust)
    .slice(0, limit);
}

// ---------- HTTP ----------
const send = (res, code, body, headers = {}) => {
  const s = JSON.stringify(body, null, 1);
  res.writeHead(code, {
    "Content-Type": "application/json; charset=utf-8",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "Content-Type,X-PAYMENT",
    ...headers,
  });
  res.end(s);
};

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://127.0.0.1:${PORT}`);
  const p = url.pathname.replace(/\/+$/, "") || "/";
  if (req.method === "OPTIONS") return send(res, 204, {});

  try {
    if (p === "/" || p === "/index.html") {
      const html = fs.readFileSync(path.join(process.cwd(), "agentlens", "public", "index.html"));
      res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Access-Control-Allow-Origin": "*" });
      return res.end(html);
    }

    if (p === "/health") {
      return send(res, 200, {
        ok: true,
        name: "AgentLens",
        version: "0.1.0",
        network: NETWORK,
        payTo: PAY_TO,
        dataGeneratedAt: index.summary.generatedAt,
        agents: index.summary.agentCount,
        services: index.summary.serviceCount,
      });
    }

    if (p === "/market") return send(res, 200, index.summary);

    if (p === "/price") {
      const cat = url.searchParams.get("category");
      if (cat)
        return send(res, 200, {
          category: cat,
          benchmark: index.summary.priceBenchByCategory[cat] || null,
          global: index.summary.marketPriceBench,
        });
      return send(res, 200, {
        global: index.summary.marketPriceBench,
        byCategory: index.summary.priceBenchByCategory,
      });
    }

    if (p === "/agents") {
      const cat = url.searchParams.get("category");
      const limit = Math.min(100, Number(url.searchParams.get("limit") || 25));
      let list = index.agents;
      if (cat) list = list.filter((a) => a.category === cat);
      return send(res, 200, {
        total: list.length,
        returned: Math.min(limit, list.length),
        agents: list.slice(0, limit).map((a) => ({
          agentId: a.agentId,
          name: a.name,
          category: a.category,
          trust: a.trust,
          score: a.score,
          reviewCount: a.reviewCount,
          usageCount: a.usageCount,
          lowestPerCallPrice: a.lowestPerCallPrice,
          lowestMonthlyPrice: a.lowestMonthlyPrice,
          priceVerdictPerCall: a.priceVerdictPerCall,
          onlineStatus: a.onlineStatus,
        })),
      });
    }

    // ---------- 付费端点 ----------
    const isTrust = p.startsWith("/trust/");
    const isCompare = p === "/compare";
    const isRecommend = p === "/recommend";
    if (isTrust || isCompare || isRecommend) {
      const key = isTrust ? "GET /trust" : isCompare ? "GET /compare" : "POST /recommend";
      const price = PRICES[key];
      const payment = req.headers["x-payment"];

      if (!payment) {
        return send(
          res,
          402,
          {
            error: "payment required",
            x402Version: 1,
            accepts: [
              {
                scheme: "exact",
                network: NETWORK,
                maxAmountRequired: price,
                resource: `http://127.0.0.1:${PORT}${p}`,
                description: key,
                mimeType: "application/json",
                payTo: PAY_TO,
                asset: USDT_XLAYER,
                assetSymbol: "USDT",
                maxTimeoutSeconds: 60,
              },
            ],
            hint: "Pay the exact USDT amount on X Layer, then retry with header X-PAYMENT: <txHash>",
          },
          { "X-PAYMENT-REQUIRED": "1" }
        );
      }

      let check;
      try {
        check = await verifyX402(payment, price, PAY_TO);
      } catch (e) {
        check = { ok: false, reason: "rpc error: " + e.message };
      }
      if (!check.ok) return send(res, 402, { error: "invalid payment", reason: check.reason });
      res.setHeader("X-PAYMENT-RESPONSE", JSON.stringify({ ok: true, paid: check.paid }));
    }

    if (isTrust) {
      const id = p.split("/")[2];
      const a = byId.get(String(id));
      if (!a) return send(res, 404, { error: "agent not found", agentId: id });
      const raw = rawById.get(String(id)) || {};
      return send(res, 200, {
        ...a,
        services: (raw.services || []).map((s) => ({
          serviceId: s.serviceId,
          name: s.name,
          price: s.price,
          priceInterval: s.priceInterval,
          salesCount: s.salesCount,
          serviceType: s.serviceType,
          freeTrial: s.freeTrial,
        })),
        reviewDistribution: raw.reviewSummary?.distribution || null,
        marketBenchmark: index.summary.priceBenchByCategory[a.category] || null,
      });
    }

    if (isCompare) {
      const a = byId.get(String(url.searchParams.get("a")));
      const b = byId.get(String(url.searchParams.get("b")));
      if (!a || !b) return send(res, 404, { error: "need valid a & b agent ids" });
      const pick = (x) => ({
        agentId: x.agentId,
        name: x.name,
        trust: x.trust,
        score: x.score,
        reviewCount: x.reviewCount,
        usageCount: x.usageCount,
        lowestPerCallPrice: x.lowestPerCallPrice,
        lowestMonthlyPrice: x.lowestMonthlyPrice,
        priceVerdict: x.priceVerdictPerCall,
      });
      const winner = a.trust >= b.trust ? a.agentId : b.agentId;
      return send(res, 200, { a: pick(a), b: pick(b), higherTrust: winner });
    }

    if (isRecommend) {
      let body = {};
      if (req.method === "POST") {
        const chunks = [];
        for await (const c of req) chunks.push(c);
        try {
          body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
        } catch {
          body = {};
        }
      } else {
        body = Object.fromEntries(url.searchParams);
      }
      const result = recommend({
        category: body.category,
        maxMonthlyPrice: body.maxMonthlyPrice != null ? Number(body.maxMonthlyPrice) : null,
        minTrust: body.minTrust != null ? Number(body.minTrust) : null,
        requireOnline: body.requireOnline === true || body.requireOnline === "true",
        limit: body.limit ? Number(body.limit) : 5,
      });
      return send(res, 200, {
        query: body,
        count: result.length,
        recommendations: result,
        marketBenchmark: body.category
          ? index.summary.priceBenchByCategory[body.category] || null
          : index.summary.marketPriceBench,
      });
    }

    send(res, 404, { error: "not found", endpoints: ["/health", "/market", "/price", "/agents", "/trust/:id", "/compare", "/recommend"] });
  } catch (e) {
    send(res, 500, { error: String(e.message || e) });
  }
});

server.listen(PORT, "127.0.0.1", () => {
  console.log(`AgentLens API → http://127.0.0.1:${PORT}`);
  console.log(`  免费: /health /market /price /agents`);
  console.log(`  x402: /trust/:id  /compare  /recommend  (payTo=${PAY_TO})`);
  console.log(`  数据: ${index.summary.agentCount} agents / ${index.summary.serviceCount} services`);
});

mcp.mjs 201 行

#!/usr/bin/env node
// mcp.mjs — AgentLens MCP server (stdio transport, zero dependencies)
//
// 让任何支持 MCP 的 Agent 直接调用 AgentLens:
//   tools: market_overview / price_benchmark / rank_agents / trust_lookup / compare_agents / recommend_agent
//
// 注册到 Claude Code:
//   claude mcp add agentlens -- node D:\Projects\am\web3\agentlens\mcp.mjs
// 或 Codex / 其他: command = node, args = ["<此文件绝对路径>"]
//
// 环境变量:
//   AGENTLENS_BASE  默认 https://agentlens.am518.uk
//   AGENTLENS_PAYMENT  可选,x402 结算交易哈希(用于付费工具)

import readline from "node:readline";

const BASE = process.env.AGENTLENS_BASE || "https://agentlens.am518.uk";
const PAYMENT = process.env.AGENTLENS_PAYMENT || "";

const TOOLS = [
  {
    name: "market_overview",
    description:
      "Market-wide stats for the OKX.AI agent marketplace: number of agents and services, total deliveries, review counts, how many agents have never delivered, and price percentiles per call and per month. Free.",
    inputSchema: { type: "object", properties: {}, additionalProperties: false },
  },
  {
    name: "price_benchmark",
    description:
      "Fair-price percentiles (p10/p25/median/p75/p90/max) for a marketplace category, so an agent can tell whether a quoted price is normal. Free.",
    inputSchema: {
      type: "object",
      properties: {
        category: {
          type: "string",
          description:
            "Optional category: SOFTWARE_SERVICES, FINANCE, LIFESTYLE, TRADING, ART_CREATION, OTHER. Omit for the global benchmark.",
        },
      },
      additionalProperties: false,
    },
  },
  {
    name: "rank_agents",
    description:
      "Ranked supply list of agent services by AgentLens trust score, with rating, review count, deliveries and price verdict. Free.",
    inputSchema: {
      type: "object",
      properties: {
        category: { type: "string", description: "Optional category filter." },
        limit: { type: "number", description: "How many to return (default 25, max 100)." },
      },
      additionalProperties: false,
    },
  },
  {
    name: "trust_lookup",
    description:
      "Full trust record for one agent: composite trust score with its components (rating, delivery, approval, liveness, freshness), shrunk rating, confidence level, every listed service with price and interval, review distribution, and the category price benchmark. Paid: 0.002 USDT per call via x402.",
    inputSchema: {
      type: "object",
      properties: { agentId: { type: "string", description: "OKX.AI agent id, e.g. 8136." } },
      required: ["agentId"],
      additionalProperties: false,
    },
  },
  {
    name: "compare_agents",
    description:
      "Head-to-head comparison of two agents on trust, rating, review count, deliveries and price verdict. Paid: 0.005 USDT per call via x402.",
    inputSchema: {
      type: "object",
      properties: {
        a: { type: "string", description: "First agent id." },
        b: { type: "string", description: "Second agent id." },
      },
      required: ["a", "b"],
      additionalProperties: false,
    },
  },
  {
    name: "recommend_agent",
    description:
      "Recommend the best agents for a task: filter by category, minimum trust, maximum per-call price and online status, then rank by trust. Returns why each candidate qualifies plus the category price benchmark. Paid: 0.01 USDT per call via x402.",
    inputSchema: {
      type: "object",
      properties: {
        category: { type: "string", description: "Category filter." },
        minTrust: { type: "number", description: "Minimum trust score 0-100." },
        maxPrice: { type: "number", description: "Maximum per-call price in USDT." },
        requireOnline: { type: "boolean", description: "Only currently online agents." },
        limit: { type: "number", description: "How many recommendations (default 5)." },
      },
      additionalProperties: false,
    },
  },
];

async function callApi(path, opts = {}) {
  const headers = { "Content-Type": "application/json" };
  if (PAYMENT) headers["X-PAYMENT"] = PAYMENT;
  const res = await fetch(BASE + path, { ...opts, headers: { ...headers, ...(opts.headers || {}) } });
  const text = await res.text();
  let body;
  try {
    body = JSON.parse(text);
  } catch {
    body = { raw: text };
  }
  return { status: res.status, body };
}

const ok = (id, result) => ({ jsonrpc: "2.0", id, result });
const err = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });

async function handleTool(name, args) {
  if (name === "market_overview") return await callApi("/market");
  if (name === "price_benchmark")
    return await callApi("/price" + (args.category ? `?category=${encodeURIComponent(args.category)}` : ""));
  if (name === "rank_agents") {
    const q = new URLSearchParams();
    if (args.category) q.set("category", args.category);
    q.set("limit", String(args.limit || 25));
    return await callApi("/agents?" + q.toString());
  }
  if (name === "trust_lookup") return await callApi(`/trust/${encodeURIComponent(args.agentId)}`);
  if (name === "compare_agents")
    return await callApi(`/compare?a=${encodeURIComponent(args.a)}&b=${encodeURIComponent(args.b)}`);
  if (name === "recommend_agent")
    return await callApi("/recommend", { method: "POST", body: JSON.stringify(args) });
  throw new Error("unknown tool: " + name);
}

const rl = readline.createInterface({ input: process.stdin, terminal: false });

rl.on("line", async (line) => {
  const s = line.trim();
  if (!s) return;
  let msg;
  try {
    msg = JSON.parse(s);
  } catch {
    return;
  }
  const { id, method, params } = msg;

  try {
    if (method === "initialize") {
      process.stdout.write(
        JSON.stringify(
          ok(id, {
            protocolVersion: "2024-11-05",
            capabilities: { tools: {} },
            serverInfo: { name: "agentlens", version: "0.1.0" },
          })
        ) + "\n"
      );
    } else if (method === "notifications/initialized" || method === "initialized") {
      // notification, no reply
    } else if (method === "tools/list") {
      process.stdout.write(JSON.stringify(ok(id, { tools: TOOLS })) + "\n");
    } else if (method === "tools/call") {
      const name = params?.name;
      const args = params?.arguments || {};
      const { status, body } = await handleTool(name, args);
      if (status === 402) {
        process.stdout.write(
          JSON.stringify(
            ok(id, {
              isError: true,
              content: [
                {
                  type: "text",
                  text:
                    "Payment required (HTTP 402). AgentLens is an x402 service on X Layer.\n" +
                    JSON.stringify(body, null, 1) +
                    "\n\nPay the exact USDT amount to payTo, then call again with the settlement transaction hash set in AGENTLENS_PAYMENT.",
                },
              ],
            })
          ) + "\n"
        );
      } else {
        process.stdout.write(
          JSON.stringify(
            ok(id, { content: [{ type: "text", text: JSON.stringify(body, null, 1) }] })
          ) + "\n"
        );
      }
    } else if (method === "ping") {
      process.stdout.write(JSON.stringify(ok(id, {})) + "\n");
    } else if (id != null) {
      process.stdout.write(JSON.stringify(err(id, -32601, "method not found: " + method)) + "\n");
    }
  } catch (e) {
    if (id != null) process.stdout.write(JSON.stringify(err(id, -32603, String(e.message || e))) + "\n");
  }
});

process.stderr.write(`agentlens MCP server ready (base=${BASE})\n`);

build-worker.mjs 57 行

// build-worker.mjs — 生成 Cloudflare Worker 部署产物
//
// 产出:
//   worker/data/index.json   市场索引(含每个 Agent 的信任分)
//   worker/data/trust.json   每个 Agent 的服务明细 + 评价分布(供 /trust 使用)
//   worker/public/index.html 看板
//
// 用法: node agentlens/build-worker.mjs

import fs from "node:fs";
import path from "node:path";

const ROOT = process.cwd();
const DATA = path.join(ROOT, "agentlens", "data");
const W = path.join(ROOT, "agentlens", "worker");
fs.mkdirSync(path.join(W, "data"), { recursive: true });
fs.mkdirSync(path.join(W, "public"), { recursive: true });

const index = JSON.parse(fs.readFileSync(path.join(DATA, "index.json"), "utf8"));
const raw = fs
  .readFileSync(path.join(DATA, "agents.ndjson"), "utf8")
  .split("\n")
  .filter(Boolean)
  .map((l) => JSON.parse(l));

const trust = {};
for (const a of raw) {
  trust[String(a.agentId)] = {
    services: (a.services || []).map((s) => ({
      serviceId: s.serviceId,
      name: s.name,
      price: s.price,
      priceInterval: s.priceInterval,
      serviceType: s.serviceType,
      freeTrial: s.freeTrial,
      symbol: s.symbol,
    })),
    reviewDistribution: a.reviewSummary?.distribution || null,
    description: (a.description || "").slice(0, 400),
  };
}

fs.writeFileSync(path.join(W, "data", "index.json"), JSON.stringify(index));
fs.writeFileSync(path.join(W, "data", "trust.json"), JSON.stringify(trust));
const html = fs.readFileSync(path.join(ROOT, "agentlens", "public", "index.html"), "utf8");
fs.copyFileSync(
  path.join(ROOT, "agentlens", "public", "index.html"),
  path.join(W, "public", "index.html")
);
fs.writeFileSync(path.join(W, "public-html.js"), "export default " + JSON.stringify(html) + ";\n");

const sz = (p) => (fs.statSync(p).size / 1024).toFixed(0) + " KB";
console.log("worker/data/index.json  ", sz(path.join(W, "data", "index.json")));
console.log("worker/data/trust.json  ", sz(path.join(W, "data", "trust.json")));
console.log("worker/public/index.html", sz(path.join(W, "public", "index.html")));
console.log(`agents=${index.summary.agentCount} services=${index.summary.serviceCount}`);

build-source.mjs 137 行

// build-source.mjs — 生成源码浏览页 + 源码压缩包(供 Worker 提供 /source 与 /source.tar.gz)
//
// 产出:
//   agentlens/worker/source-html.js   —— 导出一段 HTML(文件树 + 每个文件的内容)
//   agentlens/worker/source-tar.js    —— 导出 base64 的 tar.gz(可下载)
//
// 用法: node agentlens/build-source.mjs

import fs from "node:fs";
import path from "node:path";
import zlib from "node:zlib";

const ROOT = process.cwd();
const W = path.join(ROOT, "agentlens", "worker");

const FILES = [
  ["agentlens/README.md", "README.md"],
  ["agentlens/crawler.mjs", "crawler.mjs"],
  ["agentlens/score.mjs", "score.mjs"],
  ["agentlens/server.mjs", "server.mjs"],
  ["agentlens/mcp.mjs", "mcp.mjs"],
  ["agentlens/build-worker.mjs", "build-worker.mjs"],
  ["agentlens/build-source.mjs", "build-source.mjs"],
  ["agentlens/test-x402.mjs", "test-x402.mjs"],
  ["agentlens/refresh.ps1", "refresh.ps1"],
  ["agentlens/public/index.html", "public/index.html"],
  ["agentlens/worker/index.js", "worker/index.js"],
  ["agentlens/worker/wrangler.toml", "worker/wrangler.toml"],
  [".tools/okx-asp/okx-asp.mjs", "tools/okx-asp.mjs"],
  [".tools/okx-asp/listing.json", "tools/listing-a2mcp.json"],
  [".tools/okx-asp/listing-a2a.json", "tools/listing-a2a.json"],
];

const esc = (s) =>
  s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]));

const entries = [];
for (const [rel, shown] of FILES) {
  const p = path.join(ROOT, rel);
  if (!fs.existsSync(p)) continue;
  const content = fs.readFileSync(p, "utf8").replace(/\r\n/g, "\n");
  entries.push({ path: shown, content, lines: content.split("\n").length });
}

console.log("打包文件:");
let totalBytes = 0;
for (const e of entries) {
  totalBytes += Buffer.byteLength(e.content);
  console.log(`  ${e.path.padEnd(28)} ${String(e.lines).padStart(5)} 行`);
}
console.log(`共 ${entries.length} 个文件, ${(totalBytes / 1024).toFixed(1)} KB`);

// ---------- 源码浏览页 ----------
const nav = entries
  .map((e) => `<a href="#${encodeURIComponent(e.path)}">${esc(e.path)}</a>`)
  .join("\n      ");
const body = entries
  .map(
    (e) => `<section id="${encodeURIComponent(e.path)}">
  <h3>${esc(e.path)} <span class="m">${e.lines} 行</span></h3>
  <pre>${esc(e.content)}</pre>
</section>`
  )
  .join("\n");

const html = `<!DOCTYPE html>
<html lang="zh"><head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>AgentLens — Source</title>
<style>
  :root{--bg:#07090d;--panel:#0e131b;--line:#1e2733;--fg:#e8eef6;--dim:#8b9bb0;--accent:#4ee1a0}
  body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
  .wrap{max-width:1100px;margin:0 auto;padding:32px 20px 80px}
  h1{font-size:26px;margin:0 0 6px} h1 span{color:var(--accent)}
  p.tag{color:var(--dim);margin:0 0 20px}
  .nav{display:flex;flex-wrap:wrap;gap:8px;margin:18px 0 26px}
  .nav a{font:12px ui-monospace,Consolas,monospace;color:var(--fg);text-decoration:none;
    background:var(--panel);border:1px solid var(--line);border-radius:7px;padding:5px 9px}
  .nav a:hover{border-color:var(--accent);color:var(--accent)}
  section{margin:0 0 26px}
  h3{font:13px ui-monospace,Consolas,monospace;color:var(--accent);margin:0 0 8px;
     border-bottom:1px solid var(--line);padding-bottom:6px}
  h3 .m{color:var(--dim);font-weight:400}
  pre{background:#0a0f16;border:1px solid var(--line);border-radius:11px;padding:14px;
      overflow:auto;font:12px/1.55 ui-monospace,Consolas,monospace;color:#cfe0f2;max-height:520px}
  .dl{display:inline-block;margin-right:10px;font:12px ui-monospace,monospace;color:var(--accent);
      border:1px solid #1d4a3a;background:#0c1a16;border-radius:7px;padding:6px 11px;text-decoration:none}
  footer{color:var(--dim);font-size:12.5px;border-top:1px solid var(--line);padding-top:16px;margin-top:30px}
</style></head>
<body><div class="wrap">
  <h1>Agent<span>Lens</span> — Source</h1>
  <p class="tag">Complete source of the AgentLens trust &amp; price oracle: indexer, scoring engine, x402 API, MCP server, Cloudflare Worker, and the OKX.AI ASP registration tooling.</p>
  <div>
    <a class="dl" href="/source.tar.gz">↓ 下载 source.tar.gz</a>
    <a class="dl" href="/">← 回到看板</a>
    <a class="dl" href="/market">API /market</a>
  </div>
  <div class="nav">${nav}</div>
  ${body}
  <footer>${entries.length} files · ${(totalBytes / 1024).toFixed(1)} KB · generated ${new Date().toISOString()}</footer>
</div></body></html>`;

fs.writeFileSync(path.join(W, "source-html.js"), "export default " + JSON.stringify(html) + ";\n");
console.log("→ worker/source-html.js", (fs.statSync(path.join(W, "source-html.js")).size / 1024).toFixed(0) + " KB");

// ---------- tar.gz ----------
function tarHeader(name, size) {
  const b = Buffer.alloc(512);
  b.write(name.slice(0, 99), 0, "utf8");
  b.write("0000644\0", 100, "utf8"); // mode
  b.write("0000000\0", 108, "utf8"); // uid
  b.write("0000000\0", 116, "utf8"); // gid
  b.write(size.toString(8).padStart(11, "0") + "\0", 124, "utf8");
  b.write("00000000000\0", 136, "utf8"); // mtime
  b.write("        ", 148, "utf8"); // checksum placeholder
  b.write("0", 156, "utf8"); // typeflag
  b.write("ustar\0", 257, "utf8");
  b.write("00", 263, "utf8");
  let sum = 0;
  for (const x of b) sum += x;
  b.write(sum.toString(8).padStart(6, "0") + "\0 ", 148, "utf8");
  return b;
}
const chunks = [];
for (const e of entries) {
  const data = Buffer.from(e.content, "utf8");
  chunks.push(tarHeader("agentlens/" + e.path, data.length));
  chunks.push(data);
  const pad = (512 - (data.length % 512)) % 512;
  if (pad) chunks.push(Buffer.alloc(pad));
}
chunks.push(Buffer.alloc(1024)); // end of archive
const tar = Buffer.concat(chunks);
const gz = zlib.gzipSync(tar, { level: 9 });
fs.writeFileSync(path.join(W, "source-tar.js"), "export default " + JSON.stringify(gz.toString("base64")) + ";\n");
console.log(`→ worker/source-tar.js ${(gz.length / 1024).toFixed(0)} KB (tar ${(tar.length / 1024).toFixed(0)} KB)`);

test-x402.mjs 129 行

// test-x402.mjs — 用 X Layer 上真实的 USDT 转账交易验证 x402 支付校验逻辑
//
// 思路:链上找一笔真实 USDT Transfer → 把本地服务端 PAY_TO 临时设为该笔转账的收款地址
//       → 带 X-PAYMENT=<txHash> 调用付费端点,应当返回 200(金额足够时)
//       → 再把 PAY_TO 换成别的地址,应当返回 402(收款人不匹配)
//
// 用法: node agentlens/test-x402.mjs

import { spawn } from "node:child_process";
import fs from "node:fs";

const RPC = "https://rpc.xlayer.tech";
const USDT = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
const TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
const PORT = 8791;

async function rpc(method, params) {
  const r = await fetch(RPC, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
  });
  const j = await r.json();
  if (j.error) throw new Error(j.error.message);
  return j.result;
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

console.log("1) 拉取 X Layer 最新区块…");
const latest = Number(BigInt(await rpc("eth_blockNumber", [])));
console.log("   latest block:", latest);

console.log("2) 查找最近的 USDT Transfer 日志(RPC 限制每次最多 100 个区块)…");
let found = null;
for (let i = 0; i < 40 && !found; i++) {
  const to = latest - i * 100;
  const from = to - 99;
  let logs = [];
  try {
    logs = await rpc("eth_getLogs", [
      {
        address: USDT,
        topics: [TRANSFER],
        fromBlock: "0x" + from.toString(16),
        toBlock: "0x" + to.toString(16),
      },
    ]);
  } catch (e) {
    console.log(`   window ${from}-${to}: ${e.message}`);
    continue;
  }
  for (const lg of logs || []) {
    const recipient = "0x" + (lg.topics[2] || "").slice(26);
    const value = BigInt(lg.data === "0x" ? "0x0" : lg.data);
    if (value >= 2000n) {
      found = { txHash: lg.transactionHash, to: recipient, value, block: Number(BigInt(lg.blockNumber)) };
      break;
    }
  }
  if (i % 5 === 0 || found) console.log(`   窗口 ${from}-${to}: ${(logs || []).length} 条${found ? " → 命中" : ""}`);
}
if (!found) {
  console.log("未找到可用的 USDT 转账日志,跳过");
  process.exit(1);
}
console.log("   交易:", found.txHash);
console.log("   收款:", found.to, " 金额:", found.value.toString(), "base units");

function startServer(payTo) {
  return new Promise((resolve, reject) => {
    const p = spawn(process.execPath, ["agentlens/server.mjs"], {
      env: { ...process.env, PORT: String(PORT), PAY_TO: payTo },
      stdio: ["ignore", "pipe", "pipe"],
    });
    let out = "";
    p.stdout.on("data", (d) => {
      out += d.toString();
      if (out.includes("AgentLens API")) resolve(p);
    });
    p.stderr.on("data", (d) => process.stderr.write(d));
    p.on("error", reject);
    setTimeout(() => reject(new Error("server start timeout")), 15000);
  });
}

async function callTrust(txHash) {
  const r = await fetch(`http://127.0.0.1:${PORT}/trust/8136`, { headers: { "X-PAYMENT": txHash } });
  const body = await r.json();
  return { status: r.status, body };
}

// ---- 用例 A:PAY_TO 等于该笔转账的收款地址 → 应通过 ----
console.log("\n3) 用例 A:PAY_TO = 该笔转账收款地址(应 200)");
let srv = await startServer(found.to);
await sleep(500);
let a = await callTrust(found.txHash);
console.log("   HTTP", a.status, "|", a.status === 200 ? `name=${a.body.name} trust=${a.body.trust}` : JSON.stringify(a.body).slice(0, 160));
srv.kill();
await sleep(600);

// ---- 用例 B:PAY_TO 换成别的地址 → 应拒绝 ----
console.log("\n4) 用例 B:PAY_TO = 另一个地址(应 402,收款人不匹配)");
srv = await startServer("0x1111111111111111111111111111111111111111");
await sleep(500);
let b = await callTrust(found.txHash);
console.log("   HTTP", b.status, "|", JSON.stringify(b.body).slice(0, 160));
srv.kill();
await sleep(600);

// ---- 用例 C:伪造交易哈希 → 应拒绝 ----
console.log("\n5) 用例 C:伪造交易哈希(应 402)");
srv = await startServer(found.to);
await sleep(500);
let c = await callTrust("0x" + "ab".repeat(32));
console.log("   HTTP", c.status, "|", JSON.stringify(c.body).slice(0, 160));
srv.kill();

console.log("\n===== 结论 =====");
console.log("A 真实转账 + 正确收款地址 →", a.status === 200 ? "通过 ✅(链上校验成功)" : "失败 ❌");
console.log("B 真实转账 + 错误收款地址 →", b.status === 402 ? "拒绝 ✅" : "失败 ❌");
console.log("C 伪造哈希 →", c.status === 402 ? "拒绝 ✅" : "失败 ❌");

fs.writeFileSync(
  "agentlens/data/x402-test.json",
  JSON.stringify({ testedAt: new Date().toISOString(), transfer: { ...found, value: found.value.toString() }, A: a.status, B: b.status, C: c.status }, null, 1)
);
process.exit(0);

refresh.ps1 26 行

# refresh.ps1 — 一键刷新 AgentLens:抓取 → 评分 → 构建 → 部署
# 用法: pwsh .\agentlens\refresh.ps1

$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
Push-Location $root
try {
  Write-Host "[1/4] 抓取 OKX.AI 市场数据(断点续抓)..." -ForegroundColor Cyan
  node agentlens\crawler.mjs crawl

  Write-Host "[2/4] 计算信任分与定价基准..." -ForegroundColor Cyan
  node agentlens\score.mjs

  Write-Host "[3/4] 构建 Worker 产物..." -ForegroundColor Cyan
  node agentlens\build-worker.mjs

  Write-Host "[4/4] 部署到 Cloudflare..." -ForegroundColor Cyan
  Push-Location agentlens\worker
  npx wrangler deploy
  Pop-Location

  Write-Host "`n完成。https://agentlens.am518.uk" -ForegroundColor Green
} finally {
  Pop-Location
}

public/index.html 214 行

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>AgentLens — Trust & Price Oracle for the OKX.AI Agent Economy</title>
<style>
  :root{
    --bg:#07090d; --panel:#0e131b; --panel2:#121924; --line:#1e2733;
    --fg:#e8eef6; --dim:#8b9bb0; --accent:#4ee1a0; --accent2:#5aa9ff; --warn:#ffb454; --bad:#ff6b6b;
    --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  }
  *{box-sizing:border-box}
  body{margin:0;background:radial-gradient(1200px 600px at 20% -10%,#12202c 0%,var(--bg) 55%);color:var(--fg);
       font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
       -webkit-font-smoothing:antialiased}
  .wrap{max-width:1180px;margin:0 auto;padding:38px 22px 80px}
  header h1{font-size:30px;margin:0 0 6px;letter-spacing:-.4px}
  header h1 .dot{color:var(--accent)}
  .tag{color:var(--dim);font-size:15px;margin:0 0 4px}
  .live{display:inline-flex;align-items:center;gap:7px;font:12px/1 var(--mono);color:var(--accent);
        border:1px solid #1d3b31;background:#0c1a16;border-radius:999px;padding:5px 11px;margin-top:14px}
  .live i{width:7px;height:7px;border-radius:50%;background:var(--accent);animation:p 1.8s infinite}
  a.lnk{font:12px var(--mono);color:var(--fg);text-decoration:none;background:var(--panel);
        border:1px solid var(--line);border-radius:8px;padding:5px 11px}
  a.lnk:hover{border-color:var(--accent);color:var(--accent)}
  @keyframes p{0%,100%{opacity:1}50%{opacity:.25}}
  .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(168px,1fr));gap:12px;margin:26px 0}
  .card{background:linear-gradient(180deg,var(--panel2),var(--panel));border:1px solid var(--line);
        border-radius:13px;padding:15px 16px}
  .card .k{font:11px/1 var(--mono);color:var(--dim);text-transform:uppercase;letter-spacing:.09em}
  .card .v{font-size:26px;font-weight:650;margin-top:9px;letter-spacing:-.5px}
  .card .s{font-size:12px;color:var(--dim);margin-top:3px}
  .card.warn .v{color:var(--warn)}
  section{margin:34px 0}
  h2{font-size:17px;margin:0 0 4px;letter-spacing:-.2px}
  .hint{color:var(--dim);font-size:13px;margin:0 0 16px}
  .panel{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:18px}
  .bars{display:flex;align-items:flex-end;gap:8px;height:150px;padding-top:8px}
  .bar{flex:1;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:6px;height:100%}
  .bar .fill{width:100%;border-radius:5px 5px 2px 2px;background:linear-gradient(180deg,var(--accent2),#2b6cb0);min-height:2px;
             transition:height .5s cubic-bezier(.2,.8,.2,1)}
  .bar .fill.g{background:linear-gradient(180deg,var(--accent),#1f8f63)}
  .bar .n{font:11px var(--mono);color:var(--fg)}
  .bar .lbl{font:10px var(--mono);color:var(--dim);text-align:center;white-space:nowrap;transform:rotate(-28deg);
            transform-origin:center;margin-top:2px}
  table{width:100%;border-collapse:collapse;font-size:13.5px}
  th{text-align:left;font:11px var(--mono);color:var(--dim);text-transform:uppercase;letter-spacing:.08em;
     padding:9px 10px;border-bottom:1px solid var(--line)}
  td{padding:9px 10px;border-bottom:1px solid #131a23}
  tr:last-child td{border-bottom:none}
  td.num,th.num{text-align:right;font-family:var(--mono)}
  .trust{display:inline-block;min-width:44px;text-align:center;border-radius:7px;padding:2px 7px;
         font:12px var(--mono);font-weight:600;background:#0f2a22;color:var(--accent);border:1px solid #1d4a3a}
  .trust.mid{background:#2a2410;color:var(--warn);border-color:#4a3d18}
  .trust.low{background:#2a1414;color:var(--bad);border-color:#4a2020}
  .pill{font:11px var(--mono);padding:2px 7px;border-radius:6px;background:#151d28;color:var(--dim);border:1px solid var(--line)}
  .pill.cheap{color:var(--accent);border-color:#1d4a3a;background:#0f2a22}
  .pill.very_pricey{color:var(--bad);border-color:#4a2020;background:#2a1414}
  .pill.pricey{color:var(--warn);border-color:#4a3d18;background:#2a2410}
  code{font-family:var(--mono);font-size:12.5px;background:#111823;border:1px solid var(--line);
       border-radius:6px;padding:2px 6px;color:#b9d4f0}
  pre{background:#0a0f16;border:1px solid var(--line);border-radius:11px;padding:15px;overflow:auto;
      font-family:var(--mono);font-size:12.5px;color:#cfe0f2;line-height:1.6}
  .note{border-left:3px solid var(--accent);padding:11px 15px;background:#0c1a16;border-radius:0 10px 10px 0;
        color:#b9cfc7;font-size:13.5px;margin-top:14px}
  .two{display:grid;grid-template-columns:1fr 1fr;gap:18px}
  @media(max-width:820px){.two{grid-template-columns:1fr}}
  footer{margin-top:44px;color:var(--dim);font-size:12.5px;border-top:1px solid var(--line);padding-top:18px}
  .muted{color:var(--dim)}
</style>
</head>
<body>
<div class="wrap">
  <header>
    <h1>Agent<span class="dot">Lens</span></h1>
    <p class="tag">Trust &amp; price oracle for the OKX.AI agent economy — the machine-readable layer that tells a buyer agent <em>who is reliable and what a fair price is</em>.</p>
    <span class="live"><i></i> live index · <span id="gen">…</span></span>
    <div style="display:flex;gap:9px;flex-wrap:wrap;margin-top:14px">
      <a class="lnk" href="https://github.com/am5188/agentlens">GitHub</a>
      <a class="lnk" href="/source">Source</a>
      <a class="lnk" href="/market">/market</a>
      <a class="lnk" href="/price">/price</a>
      <a class="lnk" href="/agents">/agents</a>
    </div>
  </header>

  <div class="grid" id="kpis"></div>

  <section>
    <h2>Price dispersion</h2>
    <p class="hint">Per-call service prices span five orders of magnitude. There is no benchmark — so a buyer agent cannot tell a $0.05 API from a $5,288 one.</p>
    <div class="panel"><div class="bars" id="priceHist"></div></div>
  </section>

  <section>
    <h2>Delivery concentration</h2>
    <p class="hint">Total deliveries per agent. A quarter of listed agents have never delivered anything, yet they look identical to working ones in the marketplace.</p>
    <div class="panel"><div class="bars" id="delHist"></div></div>
  </section>

  <section class="two">
    <div>
      <h2>Category market</h2>
      <p class="hint">Supply, demand and pricing by category.</p>
      <div class="panel" style="padding:8px 6px"><table id="catTable"></table></div>
    </div>
    <div>
      <h2>Price benchmark (USDT)</h2>
      <p class="hint">What a fair price looks like, per call and per month.</p>
      <div class="panel" style="padding:8px 6px"><table id="benchTable"></table></div>
    </div>
  </section>

  <section>
    <h2>Trust leaderboard</h2>
    <p class="hint">Bayesian-shrunk rating × delivery reliability × approval rate × liveness × freshness. Small review samples are pulled toward the market mean, so a 5.0 with one review cannot outrank a 4.9 with three thousand.</p>
    <div class="panel" style="padding:8px 6px;overflow-x:auto"><table id="lb"></table></div>
  </section>

  <section>
    <h2>Machine interface (x402)</h2>
    <p class="hint">AgentLens is itself an OKX.AI service. Any MCP-capable agent can call it mid-transaction, before committing funds.</p>
    <pre>GET  /market                         free   market-wide stats &amp; benchmarks
GET  /price?category=FINANCE         free   fair-price percentiles
GET  /agents?category=&amp;limit=25      free   ranked supply list

GET  /trust/:agentId                 x402   0.002 USDT   full trust record
GET  /compare?a=&amp;b=                  x402   0.005 USDT   head-to-head
POST /recommend                      x402   0.010 USDT   task → ranked shortlist

# install as an MCP server (no npm account needed):
claude mcp add agentlens -- npx -y github:am5188/agentlens

# unpaid call returns HTTP 402:
{ "x402Version":1, "accepts":[{ "scheme":"exact", "network":"eip155:196",
  "asset":"USDT", "maxAmountRequired":"2000", "payTo":"0x…" }] }

# pay on X Layer, then retry with the settlement tx hash:
curl -H "X-PAYMENT: 0x&lt;txHash&gt;" /trust/8136</pre>
    <div class="note">Every paid response is verified on-chain: the server reads the X Layer receipt, checks a USDT <code>Transfer</code> to its own address, and only then serves the answer. No accounts, no API keys, no subscriptions — an agent pays per question.</div>
  </section>

  <footer>
    <div id="foot"></div>
    <div class="muted" style="margin-top:8px">Source: live OKX.AI Agent Plaza &amp; Task Hall index (public data). Built for OKX Dev Day 2026 · track: OKX AI — Agents and AI-native businesses.</div>
  </footer>
</div>

<script>
const fmt = (n, d = 0) => n == null ? "–" : Number(n).toLocaleString("en-US", { maximumFractionDigits: d });
const price = (n) => n == null ? "–" : (n >= 1 ? n.toFixed(2) : n.toFixed(4));
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));

async function main() {
  const m = await (await fetch("/market")).json();
  document.getElementById("gen").textContent = new Date(m.generatedAt).toISOString().replace("T", " ").slice(0, 16) + "Z";

  const kpis = [
    ["Agents indexed", fmt(m.agentCount), `${fmt(m.onlineAgents)} online`],
    ["Services", fmt(m.serviceCount), `${fmt(m.perCallServiceCount)} per-call · ${fmt(m.subscriptionServiceCount)} subscription`],
    ["Total deliveries", fmt(m.totalDeliveries), `avg ${(m.totalDeliveries / m.agentCount).toFixed(1)} per agent`],
    ["Reviews", fmt(m.totalReviews), `${fmt(m.agentsWithReviews)} agents rated`],
    ["Zero-delivery agents", fmt(m.agentsWithZeroDeliveries), `${((m.agentsWithZeroDeliveries / m.agentCount) * 100).toFixed(0)}% of supply is unproven`, "warn"],
    ["Per-call median", price(m.marketPriceBench.perCall.median) + " U", `p10 ${price(m.marketPriceBench.perCall.p10)} → max ${fmt(m.marketPriceBench.perCall.max)}`],
  ];
  document.getElementById("kpis").innerHTML = kpis.map(([k, v, s, cls]) =>
    `<div class="card ${cls || ""}"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");

  const bars = (el, data, fmtLbl, cls) => {
    const max = Math.max(1, ...data.map((d) => d.count));
    el.innerHTML = data.map((d) => {
      const h = Math.max(2, (d.count / max) * 118);
      return `<div class="bar"><div class="n">${d.count}</div><div class="fill ${cls || ""}" style="height:${h}px"></div><div class="lbl">${fmtLbl(d)}</div></div>`;
    }).join("");
  };
  const upTo = (v) => v >= 1e12 ? "∞" : (v >= 1 ? String(v) : String(v));
  bars(document.getElementById("priceHist"), m.histograms.perCall, (d) => "≤" + upTo(d.upTo));
  bars(document.getElementById("delHist"), m.histograms.deliveries, (d) => "≤" + upTo(d.upTo), "g");

  document.getElementById("catTable").innerHTML =
    `<tr><th>Category</th><th class="num">Agents</th><th class="num">Services</th><th class="num">Deliveries</th></tr>` +
    Object.entries(m.categories).map(([k, v]) =>
      `<tr><td>${esc(k)}</td><td class="num">${fmt(v.agents)}</td><td class="num">${fmt(v.services)}</td><td class="num">${fmt(v.deliveries)}</td></tr>`).join("");

  const rows = [];
  for (const [cat, b] of Object.entries(m.priceBenchByCategory)) {
    rows.push(`<tr><td>${esc(cat)}</td><td class="num">${price(b.perCall.median)}</td><td class="num">${b.monthly.median == null ? "–" : b.monthly.median.toFixed(2)}</td><td class="num">${b.perCall.n + b.monthly.n}</td></tr>`);
  }
  document.getElementById("benchTable").innerHTML =
    `<tr><th>Category</th><th class="num">/call</th><th class="num">/month</th><th class="num">n</th></tr>` + rows.join("");

  const a = await (await fetch("/agents?limit=25")).json();
  document.getElementById("lb").innerHTML =
    `<tr><th>#</th><th>Agent</th><th>Category</th><th class="num">Trust</th><th class="num">Rating</th><th class="num">Reviews</th><th class="num">Deliveries</th><th class="num">Lowest /call</th><th>Price</th></tr>` +
    a.agents.map((x, i) => {
      const t = x.trust >= 80 ? "trust" : x.trust >= 55 ? "trust mid" : "trust low";
      return `<tr><td class="muted">${i + 1}</td><td>${esc(x.name)}</td><td class="muted">${esc(x.category)}</td>
        <td class="num"><span class="${t}">${x.trust}</span></td>
        <td class="num">${x.score ?? "–"}</td><td class="num">${fmt(x.reviewCount)}</td><td class="num">${fmt(x.usageCount)}</td>
        <td class="num">${price(x.lowestPerCallPrice)}</td>
        <td><span class="pill ${esc(x.priceVerdictPerCall)}">${esc(x.priceVerdictPerCall)}</span></td></tr>`;
    }).join("");

  document.getElementById("foot").textContent =
    `${fmt(m.agentCount)} agents · ${fmt(m.serviceCount)} services · ${fmt(m.totalDeliveries)} deliveries · ${fmt(m.totalReviews)} reviews · indexed ${m.generatedAt}`;
}
main().catch((e) => {
  document.getElementById("kpis").innerHTML = `<div class="card"><div class="k">error</div><div class="v">!</div><div class="s">${esc(e.message)}</div></div>`;
});
</script>
</body>
</html>

worker/index.js 311 行

// AgentLens — Cloudflare Worker
// Trust & price oracle for the OKX.AI agent economy.
//
// 免费:  GET /  /health  /market  /price  /agents
// x402:  GET /trust/:id   GET /compare?a=&b=   POST /recommend
//
// 付费校验在链上完成:读取 X Layer 交易回执,确认向 payTo 转账了足额 USDT。

import INDEX from "./data/index.json";
import TRUST from "./data/trust.json";
import HTML from "./public-html.js";
import SOURCE_HTML from "./source-html.js";
import SOURCE_TAR from "./source-tar.js";

const USDT_XLAYER = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
const NETWORK = "eip155:196";
const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
const PRICES = { trust: "2000", compare: "5000", recommend: "10000" };

const json = (body, status = 200, extra = {}) =>
  new Response(JSON.stringify(body, null, 1), {
    status,
    headers: {
      "Content-Type": "application/json; charset=utf-8",
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Headers": "Content-Type,X-PAYMENT",
      "Cache-Control": "public, max-age=60",
      ...extra,
    },
  });

async function rpc(url, method, params) {
  const r = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
  });
  const j = await r.json();
  if (j.error) throw new Error(j.error.message);
  return j.result;
}

async function verifyPayment(env, txHash, amountRequired, payTo) {
  if (!/^0x[0-9a-fA-F]{64}$/.test(txHash)) return { ok: false, reason: "bad tx hash" };
  const rpcurl = env.XLAYER_RPC || "https://rpc.xlayer.tech";
  const receipt = await rpc(rpcurl, "eth_getTransactionReceipt", [txHash]);
  if (!receipt) return { ok: false, reason: "tx not found" };
  if (receipt.status !== "0x1") return { ok: false, reason: "tx failed" };

  const wantTo = payTo.toLowerCase().replace(/^0x/, "").padStart(64, "0");
  let paid = 0n;
  for (const lg of receipt.logs || []) {
    if ((lg.address || "").toLowerCase() !== USDT_XLAYER) continue;
    if ((lg.topics || [])[0] !== TRANSFER_TOPIC) continue;
    if (((lg.topics || [])[2] || "").toLowerCase() !== "0x" + wantTo) continue;
    paid += BigInt(lg.data === "0x" ? "0x0" : lg.data);
  }
  if (paid < BigInt(amountRequired))
    return { ok: false, reason: `paid ${paid} < required ${amountRequired}` };

  const block = await rpc(rpcurl, "eth_getBlockByNumber", [receipt.blockNumber, false]);
  const ts = block ? Number(BigInt(block.timestamp)) * 1000 : 0;
  if (ts && Date.now() - ts > 30 * 60 * 1000) return { ok: false, reason: "payment too old" };
  return { ok: true, paid: paid.toString() };
}

function recommend({ category, maxMonthlyPrice, minTrust, requireOnline, limit = 5 }) {
  let list = INDEX.agents;
  if (category) list = list.filter((a) => a.category === category);
  if (minTrust != null) list = list.filter((a) => a.trust >= minTrust);
  if (requireOnline) list = list.filter((a) => a.onlineStatus === 1);
  if (maxMonthlyPrice != null)
    list = list.filter(
      (a) => a.lowestPerCallPrice == null || a.lowestPerCallPrice <= maxMonthlyPrice
    );
  return list
    .map((a) => ({
      agentId: a.agentId,
      name: a.name,
      category: a.category,
      trust: a.trust,
      score: a.score,
      reviewCount: a.reviewCount,
      confidence: a.confidence,
      usageCount: a.usageCount,
      lowestPerCallPrice: a.lowestPerCallPrice,
      priceVerdict: a.priceVerdictPerCall,
      why: `${a.confidence} confidence (${a.reviewCount} reviews), ${a.usageCount} deliveries, price=${a.priceVerdictPerCall}`,
    }))
    .sort((x, y) => y.trust - x.trust)
    .slice(0, limit);
}

export default {
  async fetch(req, env) {
    const url = new URL(req.url);
    const p = url.pathname.replace(/\/+$/, "") || "/";
    if (req.method === "OPTIONS")
      return new Response(null, {
        status: 204,
        headers: {
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Headers": "Content-Type,X-PAYMENT",
          "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
        },
      });

    const PAY_TO = env.PAY_TO || "0x0000000000000000000000000000000000000000";

    try {
      if (p === "/" || p === "/index.html")
        return new Response(HTML, {
          headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "public, max-age=300" },
        });

      if (p === "/source")
        return new Response(SOURCE_HTML, {
          headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "public, max-age=300" },
        });

      if (p === "/source.tar.gz") {
        const bin = atob(SOURCE_TAR);
        const bytes = new Uint8Array(bin.length);
        for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
        return new Response(bytes, {
          headers: {
            "Content-Type": "application/gzip",
            "Content-Disposition": 'attachment; filename="agentlens-source.tar.gz"',
            "Cache-Control": "public, max-age=300",
          },
        });
      }

      if (p === "/health")
        return json({
          ok: true,
          name: "AgentLens",
          version: "0.1.0",
          network: NETWORK,
          payTo: PAY_TO,
          dataGeneratedAt: INDEX.summary.generatedAt,
          agents: INDEX.summary.agentCount,
          services: INDEX.summary.serviceCount,
        });

      if (p === "/market") return json(INDEX.summary);

      if (p === "/price") {
        const cat = url.searchParams.get("category");
        if (cat)
          return json({
            category: cat,
            benchmark: INDEX.summary.priceBenchByCategory[cat] || null,
            global: INDEX.summary.marketPriceBench,
          });
        return json({
          global: INDEX.summary.marketPriceBench,
          byCategory: INDEX.summary.priceBenchByCategory,
        });
      }

      if (p === "/agents") {
        const cat = url.searchParams.get("category");
        const limit = Math.min(100, Number(url.searchParams.get("limit") || 25));
        let list = INDEX.agents;
        if (cat) list = list.filter((a) => a.category === cat);
        return json({
          total: list.length,
          returned: Math.min(limit, list.length),
          agents: list.slice(0, limit).map((a) => ({
            agentId: a.agentId,
            name: a.name,
            category: a.category,
            trust: a.trust,
            score: a.score,
            reviewCount: a.reviewCount,
            usageCount: a.usageCount,
            lowestPerCallPrice: a.lowestPerCallPrice,
            lowestMonthlyPrice: a.lowestMonthlyPrice,
            priceVerdictPerCall: a.priceVerdictPerCall,
            onlineStatus: a.onlineStatus,
          })),
        });
      }

      // ---- x402 付费端点 ----
      const isTrust = p.startsWith("/trust/");
      const isCompare = p === "/compare";
      const isRecommend = p === "/recommend";
      if (isTrust || isCompare || isRecommend) {
        const key = isTrust ? "trust" : isCompare ? "compare" : "recommend";
        const price = PRICES[key];
        const payment = req.headers.get("X-PAYMENT");

        if (!payment) {
          return json(
            {
              error: "payment required",
              x402Version: 1,
              accepts: [
                {
                  scheme: "exact",
                  network: NETWORK,
                  maxAmountRequired: price,
                  resource: url.toString(),
                  description: `AgentLens /${key}`,
                  mimeType: "application/json",
                  payTo: PAY_TO,
                  asset: USDT_XLAYER,
                  assetSymbol: "USDT",
                  maxTimeoutSeconds: 60,
                },
              ],
              hint: "Pay the exact USDT amount on X Layer, then retry with header X-PAYMENT: <txHash>",
            },
            402,
            { "X-PAYMENT-REQUIRED": "1" }
          );
        }

        let check;
        try {
          check = await verifyPayment(env, payment, price, PAY_TO);
        } catch (e) {
          check = { ok: false, reason: "rpc error: " + e.message };
        }
        if (!check.ok) return json({ error: "invalid payment", reason: check.reason }, 402);

        const paidHeader = { "X-PAYMENT-RESPONSE": JSON.stringify({ ok: true, paid: check.paid }) };

        if (isTrust) {
          const id = p.split("/")[2];
          const a = INDEX.agents.find((x) => String(x.agentId) === String(id));
          if (!a) return json({ error: "agent not found", agentId: id }, 404);
          const t = TRUST[String(id)] || {};
          return json(
            {
              ...a,
              services: t.services || [],
              reviewDistribution: t.reviewDistribution || null,
              description: t.description || null,
              marketBenchmark: INDEX.summary.priceBenchByCategory[a.category] || null,
            },
            200,
            paidHeader
          );
        }

        if (isCompare) {
          const a = INDEX.agents.find((x) => String(x.agentId) === String(url.searchParams.get("a")));
          const b = INDEX.agents.find((x) => String(x.agentId) === String(url.searchParams.get("b")));
          if (!a || !b) return json({ error: "need valid a & b agent ids" }, 404);
          const pick = (x) => ({
            agentId: x.agentId,
            name: x.name,
            trust: x.trust,
            score: x.score,
            reviewCount: x.reviewCount,
            usageCount: x.usageCount,
            lowestPerCallPrice: x.lowestPerCallPrice,
            priceVerdict: x.priceVerdictPerCall,
          });
          return json(
            { a: pick(a), b: pick(b), higherTrust: a.trust >= b.trust ? a.agentId : b.agentId },
            200,
            paidHeader
          );
        }

        let body = {};
        if (req.method === "POST") {
          try {
            body = await req.json();
          } catch {
            body = {};
          }
        } else body = Object.fromEntries(url.searchParams);
        const result = recommend({
          category: body.category,
          maxMonthlyPrice: body.maxMonthlyPrice != null ? Number(body.maxMonthlyPrice) : null,
          minTrust: body.minTrust != null ? Number(body.minTrust) : null,
          requireOnline: body.requireOnline === true || body.requireOnline === "true",
          limit: body.limit ? Number(body.limit) : 5,
        });
        return json(
          {
            query: body,
            count: result.length,
            recommendations: result,
            marketBenchmark: body.category
              ? INDEX.summary.priceBenchByCategory[body.category] || null
              : INDEX.summary.marketPriceBench,
          },
          200,
          paidHeader
        );
      }

      return json(
        {
          error: "not found",
          endpoints: ["/health", "/market", "/price", "/agents", "/trust/:id", "/compare", "/recommend"],
        },
        404
      );
    } catch (e) {
      return json({ error: String(e.message || e) }, 500);
    }
  },
};

worker/wrangler.toml 15 行

name = "agentlens"
main = "index.js"
compatibility_date = "2026-09-01"
workers_dev = true
account_id = "58e2c04e69c56d0ad3ec039f56e02b01"

routes = [
  { pattern = "agentlens.am518.uk", custom_domain = true }
]

[vars]
# x402 收款地址(USDT on X Layer)— OKX Agentic Wallet (frantic@am518.uk)
PAY_TO = "0xab098996073a5bb301b422e8276bf68d962b8ae9"
XLAYER_RPC = "https://rpc.xlayer.tech"

tools/okx-asp.mjs 81 行

// okx-asp.mjs — 以参数数组方式调用 onchainos(避免 PowerShell 拆分含空格的 JSON 参数)
//
// 用法:
//   node .tools/okx-asp/okx-asp.mjs validate
//   node .tools/okx-asp/okx-asp.mjs create
//   node .tools/okx-asp/okx-asp.mjs create --force
//   node .tools/okx-asp/okx-asp.mjs <任意 onchainos 参数...>

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";

const BIN = "C:\\Users\\10671\\.local\\bin\\onchainos.exe";
const DIR = path.join(process.cwd(), ".tools", "okx-asp");

const desc = fs.readFileSync(path.join(DIR, "description.txt"), "utf8").trim();
const svc = fs.readFileSync(path.join(DIR, "listing.json"), "utf8").replace(/\r\n/g, "\n");
const picture = fs
  .readFileSync(path.join(DIR, "picture.txt"), "utf8")
  .trim();

const cmd = process.argv[2];
const extra = process.argv.slice(3);

const A2A_PATH = path.join(DIR, "listing-a2a.json");
const a2aRaw = fs.existsSync(A2A_PATH) ? JSON.parse(fs.readFileSync(A2A_PATH, "utf8")) : [];
// validate 用:去掉 operation/id
const a2aForValidate = a2aRaw.map(({ operation, id, ...rest }) => rest);
// update 用:带 operation:create
const a2aForUpdate = a2aRaw.map((x) => ({ operation: "create", ...x }));

let args;
if (cmd === "validate") {
  args = ["agent", "validate-listing", "--role", "asp", "--name", "AgentLens", "--description", desc, "--service", svc];
} else if (cmd === "validate-a2a") {
  args = [
    "agent",
    "validate-listing",
    "--role",
    "asp",
    "--name",
    "AgentLens",
    "--description",
    desc,
    "--service",
    JSON.stringify(a2aForValidate),
  ];
} else if (cmd === "update-a2a") {
  args = [
    "agent",
    "update",
    "--agent-id",
    "13437",
    "--service",
    JSON.stringify(a2aForUpdate),
    ...extra,
  ];
} else if (cmd === "create") {
  args = [
    "agent",
    "create",
    "--role",
    "asp",
    "--name",
    "AgentLens",
    "--description",
    desc,
    "--picture",
    picture,
    "--service",
    svc,
    ...extra,
  ];
} else {
  args = process.argv.slice(2);
}

console.log(">> onchainos " + args.slice(0, 2).join(" ") + ` (${args.length} args)`);
const r = spawnSync(BIN, args, { stdio: "inherit", cwd: process.cwd() });
console.log("exit:", r.status);

tools/listing-a2mcp.json 10 行

[
  {
    "serviceName": "Agent Trust & Price Oracle",
    "serviceDescription": "Ranks OKX.AI agent services by a machine-readable trust score and tells a buyer agent whether a quoted price is fair for its category, before it pays.\ncategory(string, optional): marketplace category, e.g. FINANCE, TRADING, SOFTWARE_SERVICES; minTrust(number, optional, default 0): minimum trust score 0-100; maxPrice(number, optional): maximum per-call price in USDT; requireOnline(boolean, optional, default false): only currently-online agents; limit(number, optional, default 5): how many recommendations to return\nPOST\ncurl -X POST https://agentlens.am518.uk/recommend -H \"Content-Type: application/json\" -H \"X-PAYMENT: 0x<settlementTxHash>\" -d '{\"category\":\"FINANCE\",\"minTrust\":70,\"limit\":3}'",
    "serviceType": "A2MCP",
    "fee": "0.01",
    "endpoint": "https://agentlens.am518.uk/recommend"
  }
]

tools/listing-a2a.json 10 行

[
  {
    "serviceName": "Onchain Research Report",
    "serviceDescription": "Produces an independent research report on any onchain protocol, token or market segment: revenue and fee flows, real usage versus reported usage, holder and liquidity structure, competitive position, and the specific risks that matter. Built for agent operators, DAO contributors and analysts who need a defensible read before committing capital or shipping a product.\nProvide the subject (protocol name, token, contract address or market question), the chain, and the decision you are trying to make. Optional: any data source you want prioritised.\nDelivered as a structured Markdown report with an executive summary, evidence tables, cited sources, and an explicit list of what could not be verified.",
    "serviceType": "A2A",
    "fee": "0.5",
    "subscription": []
  }
]