merchant-check-mcp

MCP serverCommerce & finance

Your AI can check whether a merchant is trustworthy before sending them any payment. merchant-check runs these pre-payment trust checks for x402 agents making payments on Base and Solana. Once added, your AI screens a seller before money moves.

Use merchant-check-mcp in Claude, ChatGPT or Ahel Desktop

Free. Sign in, add merchant-check-mcp and connect your AI. About a minute.

Also: Claude Code · Cursor · Codex

Then ask your AI: use merchant-check-mcp to check merchant

Details

Available today. Use it from your connected AI after setup.

Add merchant-check to your AI, then ask it to run a trust check on a merchant before completing a payment on Base or Solana.

merchant-check-mcpStart free

What your AI can do with it

  • Check a merchant's trustworthiness before payment is sent
  • Screen sellers ahead of x402 payments
  • Run trust checks for payments made on Base
  • Run trust checks for payments made on Solana

Install merchant-check-mcp

The server’s own address, for the clients that take one directly. Or connect Ahel once and every client you use reads it from one address, with the account kept on Ahel rather than in each client’s config.

  • Claude Code

    claude mcp add --transport http --scope user merchant-check-mcp 'https://mcp.gradientdecisions.com/mcp'

    Run it once in your project, then open /mcp to approve any sign-in the server asks for.

  • Claude Desktop

    https://mcp.gradientdecisions.com/mcp

    Add a custom connector in Settings, paste this address, and approve the sign-in.

  • Cursor

    cursor://anysphere.cursor-deeplink/mcp/install?name=merchant-check-mcp&config=eyJ1cmwiOiJodHRwczovL21jcC5ncmFkaWVudGRlY2lzaW9ucy5jb20vbWNwIn0=

    Open the link and Cursor adds the server at that address.

  • ChatGPT

    https://mcp.gradientdecisions.com/mcp

    In Settings, enable Developer mode, create an MCP app, and paste this address. Your plan and workspace must allow custom apps.

  • Codex

    codex mcp add merchant-check-mcp --url 'https://mcp.gradientdecisions.com/mcp'

    Run it once, then sign in with codex mcp login merchant-check-mcp if the server asks for an account.

From the project's README

As published by clevbot/merchant-check-mcp- in README.md.

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. See the privacy policy for how data is handled. This file tracks what's actually built, what's stubbed, and what needs you before this goes further.

Privacy policy

Live at gradientdecisions.com/privacy (src/privacy.ts), linked from the homepage footer and here. Drafted 2026-08-13 by Claude at direct request, grounded in this system's actual data practices (what query_log and merchant_signals really store — see "Internal caller-tracking dashboard" above) rather than generic boilerplate. Not reviewed by a lawyer — the page itself says so, and that caveat should stay until it has been. Contact email is info@gradientdecisions.com (switched 2026-08-19 from the developer's personal address — see "Data access policy" below for why and how). src/privacy.ts's CONTACT_EMAIL constant is the source of truth if it ever needs to change again.

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 — as of 2026-08-19 a minimal static placeholder (src/homePlaceholder.ts), not a live dashboard. It used to render every scored merchant's recommendation/signals/pricing directly, plus serve the same dataset as raw JSON at /api/wallets — see "Data access policy" below for why that was locked down. The real redesigned homepage/dashboard is a separate later task, built from Figma mockups, not this placeholder.

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.

Data access policy (2026-08-19)

Structured merchant data — recommendation, signals, pricing, reasons, platforms, anything check_merchant returns — is available only through the paid check_merchant MCP tool, paid per-query via x402. This wasn't always true, and the change is worth recording plainly:

  • / and /dashboard used to render a full live dashboard: every scored merchant, with its recommendation, signals, reasons, and pricing, straight from D1. Now a static placeholder (src/homePlaceholder.ts) with no D1 query at all.
  • /api/wallets used to serve the exact same dataset as raw JSON — no rendering, no throttling, trivially scriptable. Now returns 404 for every method and every sub-path (/api/wallets/, ?query strings, HEAD, OPTIONS, POST, ...), with Cache-Control: no-store and no CORS headers.
  • /merchant/<address> (a per-merchant profile page, added 2026-08-18 — see git history) is retired the same way, same reasoning: it returned the same fields for one wallet, and an agent calling check_merchant already has the one address it would need to read that page for free instead of paying. Code stays in src/merchantProfile.ts, unused, not deleted, in case a future paid or curated variant reuses it.

All three existed because giving humans/agents a free read of the same data check_merchant charges $0.01/query for directly undercut the product. check_merchant itself (src/tool.ts) was never affected — it has always read directly from D1 via src/db/queries.ts (getMerchantSignals, getComparablePrices, getOwnPrices), never through any of the routes above, so none of this required touching the paid tool's logic or schema.

Contact email. The privacy policy's CONTACT_EMAIL (src/privacy.ts) is info@gradientdecisions.com, replacing the developer's personal Gmail address that used to appear there and in this file. It's set up as a Cloudflare Email Routing forward to that same personal inbox — mail to info@ arrives at the existing Gmail, but the personal address itself no longer appears anywhere public-facing or committed. Email Routing is receive/forward-only: it cannot send mail as info@gradientdecisions.com. If sending-as is ever needed, that's a separate manual task (Google Workspace or custom SMTP), not something Email Routing does.

To enable it (Cloudflare dashboard, since this session has no Cloudflare API token to do it via API — see "Manual steps" note in the PR/commit this section shipped with):

  1. Cloudflare dashboard → the gradientdecisions.com zone → Email → Email Routing.
  2. Enable Email Routing for the zone. Cloudflare adds the required MX records (routing to its own mail servers) and a TXT record (v=spf1 include:_spf.mx.cloudflare.net ~all, or merged into an existing SPF record if one's already there) automatically — no manual DNS entry needed for a zone whose DNS Cloudflare already manages (true here, since custom_domain = true in wrangler.toml already put this zone on Cloudflare DNS).
  3. Add a routing rule: info@gradientdecisions.com → Destination address → the existing personal Gmail address.
  4. Cloudflare sends a verification email to that Gmail address the first time it's added as a destination — this needs a human to click the link, it can't be done programmatically. Until that's clicked, mail to info@ won't actually forward.

Additional payment protocols (researched 2026-08-19)

Prompted by Cloudflare's Agent Readiness "Commerce" checklist (ACP, AP2, MPP, UCP, x402) — the user explicitly said they don't care which protocol gets used, just that agents can actually pay, so each was researched against its real spec (not assumed) before deciding.

Shipped: GET /check (src/httpCheckEndpoint.ts) — a plain-HTTP x402 mirror of check_merchant, reusing the exact same facilitator/settlement path as /mcp. See that file's module comment for the full reasoning; short version, the MCP path's 402 challenge lives inside a JSON-RPC response body, invisible to any plain HTTP client (including Cloudflare's own scanner) that just sends a bare GET.

Researched, not implemented:

  • ACP (Agentic Commerce Protocol — OpenAI/Stripe) and UCP (Universal Commerce Protocol — Google/Shopify/Etsy/Walmart): both are full retail-checkout standards — cart construction, delegated payment tokens, tax/shipping, returns, credential providers like Google Pay. Built for buying products from a catalog, not paying per metered API call. Confirmed via their actual specs (agenticcommerce.dev, developers.openai.com/commerce, orkes.io/blog/universal-commerce-protocol-ucp-explained) — the shape mismatch is structural, not a matter of more implementation effort.

  • AP2 (Agent Payments Protocol — Google/PayPal): genuinely does support MCP as a transport, not only A2A as Cloudflare's scanner implies. Its core mechanic is cryptographically signed "mandates" — verifiable proof of a user's delegated purchase authority to an agent. Real and interesting, but solves a different problem (delegated consent for agent shopping on a user's behalf) than this service's model (an agent pays directly, no user-delegation layer needed). A proper project on its own, not a same-session addition.

  • MPP (Machine Payments Protocol — Tempo/Stripe, mpp.dev): the closest conceptual fit — its own docs describe it for "paying for an API call... buying access to data," and its usdc charge method uses EIP-3009 transferWithAuthorization, the same signing primitive x402's exact scheme already uses. The blocker: MPP's spec has no facilitator model — unlike x402 (Coinbase's CDP facilitator submits/settles for us, never touching a private key on our side), MPP expects the merchant's own server to submit the transaction on-chain and pay gas directly. Real implementation needs:

    1. A dedicated EVM wallet funded with ETH for gas, held as a new secret — a genuinely new operational responsibility (keeping it funded), unlike x402 where we never custody signing keys.
    2. Server-side relayer code: parse the Authorization: Payment credential, submit the transferWithAuthorization call via an RPC provider, poll for confirmation, handle retries/failures.
    3. A real security review before shipping — this is direct-custody, money-moving code, categorically different from x402's facilitator-mediated model.
    4. WWW-Authenticate: Payment / Authorization: Payment header handling (RFC draft-ryan-httpauth-payment) alongside the existing x402 headers.

    This is a multi-day infrastructure project (wallet funding, relayer, monitoring, security review), not a config change. Recommendation: revisit once GET /check or /mcp traffic shows real organic demand — query_log currently shows near-zero organic paid usage (see caller analytics), so a second payment rail isn't justified yet. Tracked here as a real roadmap item, not dropped.

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 2-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 (done)

Stale heading, kept for history. The workers.dev subdomain blocker that failed on every single deploy this session (wrangler reporting the account needed a subdomain before the cron trigger could attach) is resolved as of 2026-08-18 — visiting the Workers section of the Cloudflare dashboard once provisioned it. Confirmed via a live wrangler deploy:

schedule: 0 */4 * * *
schedule: 0 0 1 * *

Both cron triggers now attach cleanly. The refresh worker ran every 4 hours at the time of that deploy output above; changed to every 2 hours on 2026-08-19 (current: schedule: 0 */2 * * *, see wrangler.toml). Monthly forced re-categorization runs on the 1st. Data had gone stale for 6+ days before this was caught and fixed — worth periodically checking lastRefreshedAt on the homepage (/api/wallets is retired, see "Data access policy") even with the cron working, same as any scheduled job.

Manual refresh trigger

Not required anymore now that the cron is attached, but still useful for an on-demand refresh outside the 2-hour cadence (shared-secret header X-Admin-Token):

curl -X POST https://mcp.gradientdecisions.com/refresh \
  -H "X-Admin-Token: <your ADMIN_TOKEN>"

Internal caller-tracking dashboard

Added 2026-08-13, separate from the public merchant-scoring dashboard. GET /admin/callers (same X-Admin-Token gate as /refresh//metrics; append ?format=json for raw JSON, ?window=<seconds> to change the headline-stats window, default 7 days) tracks usage of check_merchant itself — who's calling it, how often, what they're checking — as opposed to which merchants score well. Not linked from the public site.

Purpose (verbatim from the request that added this): "this caller data is confirmed-intent ground truth (a wallet paying to check a merchant is actively evaluating a real purchase), intended to eventually inform buyer-side behavioral segmentation, not just merchant scoring." That segmentation is not built here — this only ships the tracking/ aggregation infrastructure (unique callers over time, query frequency per wallet to distinguish one-off scripts from repeat/active agents, category distribution of what's being checked, 30-day retention). Segmentation logic is explicit future work.

No-identity-resolution principle, same as everywhere else in this project: every view here is keyed on payer_address (the on-chain wallet that paid) — nothing resolves that to an off-chain identity. See src/callerDashboard.ts for the full design comment.

Implementation note: this mostly extends query_log (already existed for /metrics) rather than a new table — added queried_category (the checked merchant's category from the response) and caller_supplied_price_atomic (genuinely caller-supplied, from input.price) as two new columns, not a parallel logging path.

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

Shortened here. Read the whole README on GitHub.

Tools it offers (1)

What this server listed when Ahel dialed its public endpoint in Oct 2026, with no key and no account of yours. The names are the server’s own.

  • check_merchant

Signals

Last commit
Sep 2026
Advanced
Delivery
merchant-check MCP server → your Ahel connector (mcp.ahel.ai) → your AI.
Item type
mcp-server
Key
com-gradientdecisions-merchant-check
Source
github.com/clevbot/merchant-check-mcp-
Hosted endpoint
https://mcp.gradientdecisions.com/mcp