merchant-check-mcp

merchant-check-mcp

MCP server that scores merchant risk and reputation via a paid check_merchant tool, returning trust tiers and price-fairness readings for shopping agents.

Category
Visit Server

README

merchant-check-mcp

Gradient Decisions provides merchant intelligence for autonomous commerce. x402 Merchant Check evaluates observable on-chain payment behavior so agents can make more informed decisions before paying unfamiliar x402 merchants. Agent-native, machine-readable, x402-native — $0.01 per check, paid via x402. It is a pre-payment decision primitive, not a certification, a guarantee of safety, or a replacement for an agent's own payment policy. See INTEGRATION.md for the full discover → check → decide → pay flow, the 402-response-to-input mapping, and the exact response shape. This file tracks what's actually built, what's stubbed, and what needs you before this goes further.

DISCOVER → IDENTIFY PAYMENT DESTINATION → GRADIENT MERCHANT CHECK → AGENT PAYMENT POLICY → X402 PAYMENT

Status: two live surfaces, one product

  • Agents: https://mcp.gradientdecisions.com/mcp — the paid check_merchant MCP tool. Live on Base mainnet with real USDC (switched from Base Sepolia testnet — see "Going to mainnet"; real settled payments confirmed in query_log, real tx hashes on Base). Merchant coverage spans both Base and Solana (PayAI/Helius) — see "Solana data source". Payment flow verified end-to-end: free tools/list discovery → tools/call correctly 402s → agent builds and signs an x402 payment, submits it → facilitator settles it → real tier comes back. See "Try it yourself" below.
  • Humans: https://gradientdecisions.com — a public dashboard of every scored merchant (381 real Base wallets as of the last refresh, sourced from the x402 Bazaar — see "Data source" — plus Solana merchants from PayAI/Helius, see "Solana data source"), searchable and filterable by tier, chain, and category (see "Categorization"). Same underlying data agents pay for via MCP, free to browse. Raw JSON at /api/wallets. Chain-level trust-tier breakdowns are kept visible alongside the combined figures, not merged away — see "Solana signal caveats" for why.

Note: the two-chain merchant-signal data above is separate from this endpoint's own payment rail — check_merchant itself is still only paid via Base x402 (see "Payment flow" line above); nothing about that has changed.

Categorization

Additive to trust-tier scoring, doesn't touch src/scoring.ts. Every merchant gets a category from a fixed six-value set (src/categorize/types.ts: data_api, compute, content_generation, financial_data, storage, other) — never invented outside that set; anything the pipeline can't confidently place lands in other and is logged to category_review_log for a spot-check, not guessed.

Two passes, run once per wallet on first ingestion (not the 4-hour trust-signal cadence — a separate monthly cron force-re-runs everyone in case a listing's description changed, see wrangler.toml):

  1. Rules (src/categorize/rules.ts) — keyword match against the Bazaar listing text. Only counts as confident if exactly one category matches; zero or multiple matches (ambiguous) fall through to pass 2.
  2. Model (src/categorize/model.ts) — Claude Haiku (claude-haiku-4-5-20251001) given the fixed category list and the description, asked for exactly one value back. Response is validated against the fixed set before use — never trusted blindly; anything unparseable becomes other + logged.

ANTHROPIC_API_KEY (wrangler secret put ANTHROPIC_API_KEY) powers pass 2 — without it, every non-rule-matched description lands straight in other (logged as other_model_unavailable, not silently guessed). Set and confirmed working on 2026-08-11. POST /categorize (admin-token gated, same pattern as /refresh) processes the backlog — ?force=true re-categorizes everyone, ?limit=N caps how many per call (default 200) since a full force run across hundreds of wallets could exceed a single Worker invocation's execution time (confirmed by batching 4×100 manually).

Two real bugs found by actually running this against production, not by inspection — both fixed and redeployed:

  • Rule matching used plain substring checks, which false-matched "compute" inside "computer vision" and would have matched a bare "search" inside "research". Two real listings ("Tavily Search", "Exa /search endpoint") had been model-classified content_generation as a result of falling through to pass 2 when they should've ruled confidently to data_api. Fixed with word-boundary regex matching instead of .includes().
  • runCategorization's remaining count was wrong for force=true: since that mode's WHERE clause never excludes already-processed rows, a naive recount just reported the total every time — caught by literally watching it report the same number after 4 real batches that were each actually processing different wallets (confirmed via category_updated_at spread). Fixed by snapshotting a timestamp before each run and counting rows still older than it.

Current live distribution (2026-08-11, 375 real merchants): data_api 145, other 93 (genuinely ambiguous now, not "model unavailable"), financial_data 59, content_generation 45, compute 21, storage 4, 8 legitimately uncategorized (stale, delisted from Bazaar since the last refresh — untouched by design, not a bug).

Try it yourself

npm run demo

Stale as of 2026-08-11 — the deployed server only accepts Base mainnet now (X402_NETWORK = eip155:8453, see "Going to mainnet"). This script still pays on Base Sepolia, so it'll fail against the live deployment; kept as reference and for anyone testing a testnet-configured branch locally. For a real, working live test, see "Mainnet live payment test" below.

Uses the throwaway keypair in .env.demo (gitignored, testnet-only, zero real value). To get past the "insufficient balance" step and see an actual paid trusted/avoid response on a testnet deployment:

  1. Get the payer address: DEMO_PAYER_ADDRESS in .env.demo (0x9AaF5bB90307bacb9cB60f54c1be2B65B0771282).
  2. Fund it with Base Sepolia test USDC: faucet.circle.com (select Base Sepolia).
  3. Re-run npm run demo. The two seeded wallets (0x1111...11d1 / 0x2222...22d2, inserted directly into D1 for this demo — see "Demo data" below) should come back trusted and avoid respectively, with a real settlement tx hash.

Mainnet live payment test

Real money. scripts/mainnet-live-test.ts makes one real $0.01 x402 payment against the live mainnet deployment — the actual proof that settlement genuinely works end to end, not just that the facilitator config resolves correctly (which was already verified separately without spending anything). Deliberately not wired into npm run demo or any other default command — only runs via the explicit npm run mainnet-test, and only with a private key you provide via a local, gitignored .env.mainnet-test file that never leaves your machine (same pattern as every other secret in this project — I don't generate, hold, or touch it).

cd "/Users/colincleven/Documents/merchant-check-mcp"

# 1. Generate a fresh throwaway keypair (runs locally, nothing sent anywhere)
PATH="/Users/colincleven/.nvm/versions/node/v24.15.0/bin:$PATH" node --input-type=module -e "
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
const k = generatePrivateKey();
const a = privateKeyToAccount(k);
console.log('address:', a.address);
console.log('private key:', k);
"

# 2. Save the private key locally (paste the value the command above printed)
cat > .env.mainnet-test << 'EOF'
MAINNET_PAYER_PRIVATE_KEY=paste_the_private_key_here
EOF

# 3. Send a small amount of real USDC on Base mainnet (e.g. $0.05) to the
#    "address:" printed in step 1, from your own wallet/exchange.

# 4. Run the real test
PATH="/Users/colincleven/.nvm/versions/node/v24.15.0/bin:$PATH" npm run mainnet-test

Checks a real trusted-tier merchant from the live dataset by default (0xffc458db291b4abce020fe3de4f91f2770e537b1) — override with TEST_MERCHANT_WALLET=0x... npm run mainnet-test. Success prints a real transaction hash and a BaseScan link.

Demo data

merchant_signals has two synthetic rows I inserted directly via wrangler d1 execute --remote — clearly fake addresses (0x11111111111111111111111111111111111111d1, 0x22222222222222222222222222222222222222d2), not real merchants, kept around because there's still no real avoid example (see "Still needed" below). They're marked is_demo = 1 and tagged network = 'eip155:84532' (Base Sepolia — the network the demo/test payment flow actually runs on, vs. eip155:8453 Base mainnet for every real BazaarDataSource row). Two separate columns on purpose: is_demo marks fake rows, network marks which chain real rows came from — different failure modes to guard against (synthetic data vs. real testnet data leaking into the mainnet dataset), so both stay explicit instead of collapsing into one flag. Both filters (WHERE is_demo = 0 AND network = 'eip155:8453') currently produce an identical result set, but that changes the moment any real data source can observe testnet activity. Excluded from the public dashboard and /api/wallets (src/dashboard.ts) — gradientdecisions.com only ever shows real mainnet data — but check_merchant itself still sees them, so npm run demo keeps exercising all three tiers. (An earlier version of these two addresses was 38 hex characters instead of 40 — isValidWalletAddress's own regex rejected them, so every demo call came back "not a valid EVM address" even though payment settled fine. Verify address length programmatically, not by eye — see git history.)

Known issue found and fixed during deployment

resource.serviceName in src/index.ts's createPaymentWrapper config must be printable ASCII only (no em-dash) and ≤32 characters — @x402/core's ResourceInfoSchema rejects anything else with a ZodError, which silently broke payment-required detection on the client side (the malformed response just looked like an inert error result, not something worth auto-paying for). Found by testing against the live deployment, not from any docs — worth knowing if you add more resource metadata elsewhere.

Remaining one-time account setup

  • workers.dev subdomain: the scheduled refresh-worker cron trigger still fails to attach on deploy — wrangler reports the account needs a workers.dev subdomain enabled first (one-time, one click: open the Workers section of the Cloudflare dashboard once). Not currently blocking anything else — see "Manual refresh trigger" below for the workaround — but re-run wrangler deploy after enabling it to attach the cron and stop needing that workaround.

Manual refresh trigger

Until the cron above is attached, POST /refresh (shared-secret header X-Admin-Token, value in .env.demo as ADMIN_TOKEN) runs the refresh worker on demand:

curl -X POST https://mcp.gradientdecisions.com/refresh \
  -H "X-Admin-Token: $(grep ADMIN_TOKEN .env.demo | cut -d= -f2)"

Useful permanently too, even after the cron works, for an on-demand refresh outside the 4-hour cadence.

Phase 0 resolution (stack compatibility)

Confirmed via Cloudflare's own docs and by actually installing/typechecking against the real packages (not just reading about them):

  • MCP transport: @modelcontextprotocol/sdk's WebStandardStreamableHTTPServerTransport — a fetch()/Request/Response-based transport whose own JSDoc includes a Cloudflare Workers usage example. Runs natively on Workers, no polyfills.
  • Payments: @x402/core + @x402/evm + @x402/mcp — the official x402-foundation/Coinbase packages (same publishers as the protocol spec itself). Not x402-hono, and not Cloudflare's own agents package — see "Why not x402-hono" and "Why not Cloudflare's agents package" below.
  • Data store: D1 (per brief) — relational joins across payer wallets and price observations need more than KV's key-value model gives you.

Net result: no fallback to Vercel/Railway was needed. Cloudflare Workers works for both legs.

Why not x402-hono

x402-hono (and the framework-middleware family generally) gates by HTTP route. MCP puts every JSON-RPC method — initialize, tools/list, tools/call — on a single POST endpoint. Gating the whole route would put tools/list behind a paywall too, which breaks agent discovery (the brief's own GTM plan depends on agents being able to read the tool description before deciding to pay). @x402/mcp's createPaymentWrapper instead wraps a specific tool handler, so discovery stays free and only check_merchant's actual execution is metered.

Why not Cloudflare's agents package

The initial plan (before checking) was Cloudflare's agents package (createMcpHandler, built on @modelcontextprotocol/server). Once actually installed, @x402/mcp turned out to depend directly on @modelcontextprotocol/sdk (a different, if related, package) — and there's no documented integration between @x402/mcp and agents' MCP server wrapper. Rather than combine two payment-adjacent SDKs in a way nobody's documented, this uses @modelcontextprotocol/sdk directly, exactly as @x402/mcp's own README examples do. agents isn't a dependency here at all.

Platform / website info

Added 2026-08-12. check_merchant and the dashboard now surface the actual resource URL(s) (and service name, where the discovery feed gives one) a merchant wallet backs — e.g. https://api.example.com/v1/weather — not just the raw wallet address. This isn't new data collection: both Bazaar and PayAI already return a resource URL + serviceName per listing, it was previously only ever blended into bazaar_description's free-text blob (for categorization) and thrown away otherwise. MerchantSignalRow. platforms_json (JSON array of {url, serviceName}, one wallet can back several resources) stores it structured now; CheckMerchantOutput.platforms surfaces it to callers; the dashboard's Platform column links to it directly. NULL/empty until a wallet's been ingested at least once — same status as category, not a scoring input.

What's built

  • src/index.ts — Worker entry point, routed by pathname (not hostname, so it works on the workers.dev fallback URL too): /mcp wires x402ResourceServer + ExactEvmScheme + createPaymentWrapper around check_merchant over WebStandardStreamableHTTPServerTransport; / and /dashboard serve the human dashboard; /api/wallets the same data as JSON; /refresh the admin-gated manual refresh trigger.
  • src/dashboard.ts — public dashboard (served at gradientdecisions.com, same Worker as the MCP endpoint at mcp.gradientdecisions.com). Self-contained HTML/CSS/JS, no external dependencies, light/dark aware, client-side search + tier filter. Excludes is_demo = 1 rows — see "Demo data".
  • src/tool.tscheck_merchant logic. Reads only from D1 (merchant_signals, price_observations) — no chain access on the paid request path, per the brief's precomputed-store requirement.
  • src/scoring.ts — rules-based v1 composite. Every reasons entry maps to one named signal; thresholds are constants at the top of the file.
  • src/refresh/index.ts — scheduled worker (cron: every 4 hours, see wrangler.toml) that aggregates raw activity into merchant_signals rows.
  • src/refresh/indexer.tsChainDataSource interface + BazaarDataSource, a real (not stubbed) implementation against the public x402 Bazaar. See "Data source" below.
  • src/refresh/solana-indexer.tsPayAIDataSource, the Solana counterpart to BazaarDataSource: PayAI discovery + optional Helius payer-diversity augmentation. See "Solana data source" below.
  • src/chains.ts — chain detection/address normalization shared by every module that touches a wallet address, now that the store holds both Base (0x-hex, case-insensitive) and Solana (base58, case-sensitive) wallets in one table.
  • db/schema.sql — D1 schema. Applied to the live remote merchant-signals D1 database (wrangler d1 execute --remote).
  • scripts/backtest.ts + scripts/labeled-wallets.json — the brief's required validation step, before charging for real queries.

What's deliberately stubbed

  • Signal 6 (velocity/harness-break anomalies): per your 2026-08-10 decision, the buyer-side wallet-harness pipeline doesn't exist yet, so detectVelocityAnomalyStub() in src/refresh/index.ts always returns "no anomaly" rather than a fabricated heuristic that would silently miscalibrate the tier logic. Replace its body once that pipeline exists.
  • Payer-clustering (signal 2's cluster flag): same file, hardcoded to 0. A real implementation needs to check whether payer wallets share funding sources or were created in a burst — out of scope until there's a real data source to check it against.
  • Wallet age (signal 1) and refunds (signal 4): BazaarDataSource can't see these (see "Data source" below) — every Bazaar-sourced row has wallet_age_days = null and refund_count = 0, so scoreMerchant() never flags either for real data yet. Not fabricated as "fine", just unmeasured.
  • Price variance (signal 5): same reason — BazaarDataSource never populates price_observations, so computePriceVarianceFlag() always returns 0 for real data. The logic itself is real and already wired up; it activates for free once a source that can populate this exists.

Data source

BazaarDataSource (src/refresh/indexer.ts) — real, not stubbed. Pulls from the x402 Bazaar, Coinbase's own facilitator discovery catalog (GET https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources) — public, no account or API key needed. Confirmed live: as of 2026-08-10 it has ~14,500 registered resources; a refresh run indexed 365 unique Base- mainnet merchant wallets from the first 2,000, giving real total_tx_count / unique_payer_count from Coinbase's own 30-day call-volume and unique-payer metrics per merchant.

Real scope limits (see "What's deliberately stubbed" above for exactly which signals this affects): only covers merchants who've registered a resource on Bazaar, not every wallet that's ever received an x402 payment; no first-activity timestamp; no settlement-completion or refund visibility from a directory listing; Bazaar's "resource" (an API endpoint) doesn't map to the goods/services resource_type buckets check_merchant's price-fairness check uses.

Filling those gaps means either Coinbase's CDP wallet-history API (needs a free CDP account + API key at portal.cdp.coinbase.com — account creation has to be you, not me) or a custom chain indexer. Both are future work, not blocking anything currently running. FixtureDataSource is also available for local testing without live network access.

Solana data source

Added 2026-08-12 (requirement: Solana carries real, live x402 volume today, not just testnet activity, so it feeds the same trust-tier scoring Base does — see src/refresh/solana-indexer.ts for the full implementation writeup). Two pieces:

Discovery — PayAIDataSource. The brief's original plan was "Solana's x402 Agent Registry," but that name doesn't correspond to a real merchant catalog: solana.com/agent-registry turned out to be a buyer-side agent- identity product, not merchant discovery. The real functional equivalent is PayAI Network's facilitator discovery feed (https://facilitator.payai.network/discovery/resources) — confirmed live via direct curl during research: 25,928 total items, a mixed Base+Solana catalog in the same discovery-list JSON shape as x402 Bazaar. Free, public, no key. Unlike Bazaar, it has no quality field on any sampled item — no call-volume or unique-payer counts ship with the listing itself.

Payer-diversity augmentation — Helius (optional, HELIUS_API_KEY). Fills the gap PayAI's discovery feed leaves. For each Solana merchant wallet PayAI surfaces, counts real USDC (SPL) transfers to that wallet via Helius's Enhanced Transactions API (mainnet.helius-rpc.com — not api.helius.xyz, which 401s; Helius moved this endpoint onto their RPC host, found the hard way via a live failed refresh, confirmed against their current docs) and the unique source addresses behind them — capped at 8 wallets, 1 page (100 txs) per wallet per refresh run. That cap is not Helius's free-tier limit (1M credits/month comfortably covers far more); it's this Cloudflare account's real, confirmed-live Worker subrequest budget: ~50 external fetches per invocation, shared across BazaarDataSource (≤20 pages), PayAIDataSource's own discovery pages (≤5), Helius augmentation, and any first-time-wallet categorization calls to Anthropic (capped separately at 8 per run in src/refresh/index.ts) — all in the same invocation, since runRefresh runs every source back-to-back. First deploy of this feature hit exactly this ceiling (Too many subrequests by single Worker invocation, caught via wrangler tail against a real production request, not simulated) before these caps existed. Raising Cloudflare's Workers plan (Bundled/Paid raises the ceiling to 1000) would remove the need for this tight a budget — a real option, left as a billing decision for you rather than done here. Wallets beyond the cap, or with HELIUS_API_KEY unset entirely, keep PayAI's bare listing (usually 0 calls / 0 payers) — scoreMerchant() reads that as insufficient data, not a trust signal either way, never a guess.

Why Helius and not x402scan's paid API. x402scan (x402scan.com) is a chain-agnostic x402 explorer with its own real per-call paid API ($0.01–0.02/call via x402, confirmed from its own OpenAPI spec) that would give richer, x402-specific merchant/transaction data than raw Helius transfer-counting can. Decided against it for this v1 for a concrete reason, not a blanket "avoid paid data" stance — using it would mean this backend autonomously holding and spending from a funded wallet on a schedule, a custody/architecture commitment bigger than "is $5–15/month reasonable." Helius's free tier gets real, live Solana payer-diversity data shipped now without that commitment. This isn't a closed door: once Helius-based data is live, the plan is to compare what it actually delivers against what x402scan's paid API would add on top, with real numbers instead of speculation, and revisit from there — see conversation history 2026-08-12 for the fuller reasoning (the business-model symmetry point: an aggregator that itself charges for data isn't inherently wrong to pay a nominal fee for better upstream data — the open question is what marginal value it buys over what's already free, not whether spending is acceptable in principle).

Solana signal caveats

Referenced from db/schema.sql's velocity_anomaly_flag comment and src/scoring.ts's per-signal cross-chain read — collected here in one place:

  • Helius counts any USDC transfer, not specifically x402 payments. A merchant receiving USDC through some other channel (a direct transfer, an unrelated payment app) looks like extra x402 volume. Bazaar has the mirror-image gap on Base (undercounting — it only sees registered listings). Neither source is ground truth; both are documented approximations.
  • firstSeenAt is window-bounded, not true wallet age, for Solana rows — it only reflects the earliest transfer within the 90-day lookback and the 3-page-per-wallet Helius cap, same null-vs-approximate tradeoff Bazaar- sourced Base rows already have for this signal (Bazaar gives no first-activity timestamp at all).
  • Fee-payer sponsorship is NOT a misattribution risk. Solana's x402 "exact" scheme cryptographically excludes the fee-payer from being transfer source/authority/destination (see specs/schemes/exact/scheme_exact.md in x402-foundation/x402) — a counted transfer's source address is always the real payer, never a sponsoring relayer. Verified against the spec directly, not assumed.
  • Signal 6 (velocity/harness-break) is the one signal known NOT to translate once implemented, not just currently stubbed like it is on Base. Solana settles roughly 4x faster (~0.5s vs Base's ~2s) — a transaction-frequency threshold tuned on Base traffic would over-flag entirely normal Solana activity as anomalous. Whoever builds this signal needs Solana-specific thresholds, not Base's reused unmodified.
  • Signal 2's diversity-ratio threshold (LOW_PAYER_DIVERSITY_RATIO = 0.3 in src/scoring.ts) is Base-calibrated, not cross-chain-validated. The ratio itself (unique_payers / total_tx) is dimensionless and should translate in principle, but the specific cutoff was derived from 365 real Base Bazaar merchants only. Treat it as an assumption until enough Helius-augmented Solana rows exist to check it against real Solana payer distributions.
  • USDC decimals match across chains (both 6) — cross-checked against x402scan's own facilitator constants and Solana's official USDC mint registry — so atomic-unit price comparisons in db/queries.ts getComparablePrices are valid across Base and Solana within the same category without any conversion step. This is the one place cross-chain comparison is intentionally pooled rather than kept separate — see that function's own comment for why.

Done vs. still needed

Corrected 2026-08-12 — most of this list was stale, written back when the deployment was still testnet-only; several "still needed" items below had actually already been completed and the list hadn't been updated to say so. Verified against the live deployment before rewriting, not just edited from memory.

Done:

  • ✅ Cloudflare account authenticated (wrangler login, colin.cleven@gmail.com).
  • ✅ D1 database created (merchant-signals) and schema applied remotely.
  • Live on Base mainnet, real PAYOUT_ADDRESS — confirmed via real settled mainnet transactions with real tx hashes in query_log (I never saw or handled the actual address value, consistent with this project's security practice throughout).
  • ✅ Deployed to mcp.gradientdecisions.com (custom_domain = true in wrangler.toml auto-provisioned DNS + SSL since the zone was already on this Cloudflare account).
  • ✅ Two synthetic demo rows seeded into merchant_signals (see "Demo data").
  • ✅ Real data source wired and run: BazaarDataSource indexes real Base mainnet merchant wallets from the public x402 Bazaar (no account needed).
  • ✅ Backtest passes against real data: 2/2 cases (trusted + caution, both real Bazaar merchants — see scripts/labeled-wallets.json). avoid is still explicitly unvalidated — no real bad-actor source exists yet (see _avoid_bucket in that file for why a thin-history wallet isn't a valid stand-in) — genuinely still open, not corrected here.
  • ✅ Solana added as a second data source (2026-08-12): chain column migrated onto remote D1, PayAIDataSource + Helius wired into runRefresh() alongside BazaarDataSource, check_merchant output and the dashboard both surface chain, tool descriptions rewritten for semantic-intent matching mentioning both chains, platforms (merchant website/API URLs) surfaced in output and dashboard — see "Solana data source" and "Platform / website info" above.
  • HELIUS_API_KEY set and confirmed working — real Solana payer-diversity data flowing (see "Solana data source").
  • ANTHROPIC_API_KEY set 2026-08-11, confirmed working, categorization backlog processed.
  • ✅ Registry submission — live on the official MCP registry (com.gradientdecisions/merchant-check) and indexed by third-party directories (e.g. mcp.so) as a result. Worth a periodic check: these directories may cache descriptions and not immediately reflect README/tool changes made here — if something there looks stale, it's a caching lag on their end, not necessarily a stale source here, but check both.

Still genuinely needed:

  1. workers.dev subdomain — one dashboard click (Cloudflare dashboard → Workers menu, opening it the first time auto-provisions one) to attach the cron trigger. Every deploy this session has logged the same "You need a workers.dev subdomain" error for the cron schedule specifically — MCP traffic and the dashboard are unaffected, only the automatic 4-hour refresh; POST /refresh remains the working manual trigger until this is done.
  2. A real avoid example for the backtest — needs either a genuine x402-specific bad-actor source (none found publicly — the tech's too new) or enough real usage data over time to observe one organically.

Going to mainnet (done)

Stale heading, kept for history — this already happened. X402_NETWORK in wrangler.toml is "eip155:8453" (Base mainnet), not the Sepolia testnet value this section originally described. PAYOUT_ADDRESS receives real USDC from real callers; query_log has real settled Base mainnet transactions with real tx hashes, confirmed directly against remote D1 (see "Try it yourself" above). If this ever needs to move back to testnet for local dev, the value to change is the same one-line X402_NETWORK edit this section originally documented, just in reverse.

Local dev limitation (this machine)

wrangler dev / wrangler d1 execute --local need workerd, which requires macOS 13.5.0+; this machine is on 12.6.0, so neither ran here. Schema syntax was instead verified directly with sqlite3 (D1 is SQLite-compatible) — all three tables created cleanly. npm run typecheck passes end-to-end. Actual runtime testing (wrangler dev, then hitting /mcp with a real MCP client) needs either a newer macOS, a Linux devcontainer, or testing directly against wrangler dev --remote / a deployed Worker.

Known rough edge

query_log (usage/revenue visibility, not a scoring input) logs "settled" as a placeholder tier_returned instead of the real tier — @x402/mcp's onAfterSettlement hook doesn't have access to the tool handler's return value, only payment/settlement info. Fine for v1; see the comment in src/index.ts if this ever needs to carry the real tier.

category and price_fairness now actually reach agents

Until 2026-08-11, category existed only in D1/the dashboard — check_merchant's response never included it — and price_fairness was a permanent "unknown" stub for every real merchant, since nothing had ever populated price_observations. Both fixed: category is in the tool's output now, and price_fairness compares a merchant's price against real peers in its own category (not the old caller-supplied resource_type, which nothing ever populated data for — kept in the input schema for compatibility, documented as unused).

Three real bugs surfaced getting this actually working end-to-end against production (each found via wrangler tail against a real failing request, not by inspection):

  1. Per-wallet D1 writes for price data (up to one INSERT per resource, some wallets have 65+) blew through D1's 1000-queries-per-invocation cap across ~370 wallets in one refresh. Fixed by restructuring to bulk operations — one upfront category lookup instead of 370, price rows accumulated in memory and written as ~20 chunked statements at the end.
  2. D1's real bound-parameter limit is ~100/statement, not SQLite's usual 999, and not stated in D1's own error message. env.DB.batch() sums params across every statement in the call against that same ceiling — batching multiple large inserts together doesn't dodge it, only fewer total bound params per individual statement does.
  3. getComparablePrices sorted by observed_at DESC for recency, but every row from one bulk refresh shares the exact same timestamp — sorting a fully-tied key returns an arbitrary, non-representative subset. A real merchant priced 50-500x below its category's true median still came back "high", because the LIMIT 200 subset it landed on was itself skewed low. Fixed with ORDER BY RANDOM().

Verified live: a real data_api merchant's category appears correctly in check_merchant's output, and price_fairness resolves fair/low/high correctly around the real computed median (~$0.01, from 735 real comparable observations) for that category.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured