MCP server: One MCP server.

Agentic Endpoints

@oliverkissRun by its publisher: served from their own domain, namespace checked by the registry.MCP server · External

Pay-per-call tools for autonomous agents, settled in USDC on Base via x402.

Agent collaborationv1.0.0MITTypeScript
Freeno charge per call

One address for any MCP client. No sign-in needed on this server.

What it does

8 read3 act9 other

Grouped by tool name. At most: moves money, runs code or commands, deletes data, or sends messages for you.

What it costs

Free

Odel charges nothing per call. The publisher may charge for its own service.

Tools

20 · grouped by tool name · checked 26 Sep 2026

Read · 8Look things up

  • meetings_search
  • x402_verify
  • meetings_summarize
  • meetings_get
  • meetings_list
  • vault_retrieve
  • vault_list
  • pdf_parse

Act · 3Change something

  • meetings_import
  • vault_store
  • vault_delete

Other · 9Neither hinted nor named

  • once_key_claim
  • once_key_complete
  • once_key_release
  • vault_exists
  • vault_rotate_token
  • scrape
  • compress
  • credits_trial
  • credits_balance
Try it in the Inspector
README48 min read

x402-powered micro-SaaS utilities for autonomous AI agents. Pay-per-call with USDC micropayments on Base — no API keys, no accounts, no subscriptions.

Live at https://agentic-endpoints.com

Endpoints

RouteMethodPriceDescription
/once-keyPOST$0.010Claim an action exactly once, and replay its recorded result
/once-key/completePOSTFreeRecord the outcome of a claimed action
/once-key/releasePOSTFreeSurrender a claim whose work failed
/scrapePOST$0.010Web scraping and text extraction
/pdf-parsePOST$0.020PDF text extraction from a URL
/compressPOST$0.010Token compression / context reduction for LLMs
/x402/verifyPOST$0.010Check a stranger's x402 endpoint before paying it
/reachPOST$0.010Can agents reach a URL? One GET as each of nine common clients
/reach/reportPOST$69.00Full reachability report for one site: up to 20 pages, causes, fixes, 3 re-scans
/reach/report/status, /reach/report/rescanPOSTFreeRead a report; re-scan it after fixing
/reach/report/{id}GETFreeThe report as a web page
/meetings/importPOST$0.010Import a meeting transcript, private or searchable
/meetings/searchPOST$0.010Full-text search across your meetings
/meetings/summarizePOST$0.050Answer a question from your meetings, with citations
/meetings/getPOST$0.005Read one meeting in full
/meetings/listPOST$0.005List meetings (metadata only)
/meetings/deletePOST$0.001Delete a meeting and its index entry
/vault/storePOST$0.02Store a client-encrypted item
/vault/retrievePOST$0.02Retrieve a client-encrypted item
/vault/deletePOST$0.005Delete an item
/vault/existsPOST$0.005Check whether an item exists
/vault/listPOST$0.005List the keys in a namespace (metadata only)
/vault/rotate-tokenPOSTFreeReplace a namespace token that may have leaked
/credits/buyPOST$5.00Buy $6.00 of prepaid credit (20% bonus)
/credits/buy-25POST$25.00Buy $32.50 of prepaid credit (30% bonus)
/credits/balancePOSTFreeCheck a credit balance
/credits/trialPOSTFree$0.10 of credit, once, with no wallet and no account
/revenueGETFreeOn-chain USDC received, read from Base
/mcpPOSTFree to listRemote MCP server; each tool costs its route's price
/a2aPOSTFree to listA2A JSON-RPC transport; each skill costs its route's price
/GETFreeService discovery (JSON) or landing page (HTML)
/healthGETFreeHealth check
/statusGETFreeUptime, error rate and latency, derived from recorded behaviour
/statsGETFreeDemand funnel: challenged, paid, free, by route (buyer details withheld)
/llms.txtGETFreeProse description for a model given only a URL
/openapi.jsonGETFreeOpenAPI 3.1 description
/.well-known/agent-card.jsonGETFreeA2A Agent Card (all 21 skills)
/terms, /privacy, /refunds, /acceptable-use, /complianceGETFreePolicies, as HTML
/how-it-works, /faq, /aboutGETFreeHuman-facing pages: paying, common questions, who runs it
/.well-known/compliance.jsonGETFreeThe same policies, machine-readable
/admin/stats, /admin/takedownGET, POSTOperator onlyFull buyer detail, and acting on abuse reports. 404 without ADMIN_TOKEN

GET / content-negotiates: send Accept: application/json for the machine-readable endpoint catalogue, anything else gets the HTML landing page.

How It Works

  1. An agent requests a paid endpoint
  2. The server replies HTTP 402 Payment Required with the price, network and receiving address
  3. The agent signs a USDC transfer on Base and retries with a PAYMENT-SIGNATURE header (x402 v2) or X-PAYMENT (v1); both are accepted, directly and through MCP and A2A
  4. The payer's address is screened against a sanctions list; a listed payer gets 451 and nothing settles
  5. The facilitator verifies and settles the payment, then the handler runs

Try it — this returns a real 402 challenge, not an error:

bash
curl -i -X POST https://agentic-endpoints.com/once-key \
  -H 'Content-Type: application/json' \
  -d '{"namespace":"demo-4f9c2b1e8d7a","action_key":"abc123"}'

Try It Free

Funding a wallet to find out whether a service is any good is the wrong order, and it was the single largest barrier in front of every paid route here. So POST /credits/trial hands out $0.10 of credit — about 10 calls — with no wallet, no signature and no account:

bash
TOKEN=$(curl -sX POST https://agentic-endpoints.com/credits/trial | jq -r .credit_token)
curl -X POST https://agentic-endpoints.com/compress \
  -H "X-Credit-Token: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"text":"...","target_tokens":50}'

The token is not issued and recorded; it is derived from the caller's address by HMAC, so asking twice returns the same ledger with whatever is left on it — never a refill. That needs no issuance table and no new Durable Object, and it is safe to retry, which matters because the caller is an agent that will retry. IPv6 addresses collapse to their /48, the block one customer is typically delegated, so a single customer cannot walk their subnets for more; and at most 500 new trials are granted per UTC day across the whole service (TRIAL_DAILY_CAP), whatever the address rule lets through. Every 402 advertises the trial in an X-Trial-Available header, so an agent that hits the paywall learns the way around it in the same response.

Over MCP the same trial is a tool. An MCP client controls a tool's arguments but usually cannot set a per-call HTTP header, so credits_trial issues the token and every paid tool accepts it as an optional credit_token argument — meaning a client with no wallet can go from tools/list to real work without ever setting a header. A refused tool call names that route explicitly rather than only quoting x402 at a caller that cannot sign.

Agent-to-Agent (A2A)

The service speaks A2A v0.3 at https://agentic-endpoints.com/a2a, with an Agent Card at /.well-known/agent-card.json (and the legacy /.well-known/agent.json).

A2A has no payment step, which is why agents publishing x402 services usually declare their paid work out of scope for A2A and expose only the free parts. Credit here is a header rather than a protocol negotiation, so every paid skill is callable over A2A in a single round trip — all 21 of them — and the free trial means the caller needs no wallet to start:

bash
curl -X POST https://agentic-endpoints.com/a2a -H 'Content-Type: application/json' \
  -H "X-Credit-Token: $TOKEN" -d '{
    "jsonrpc":"2.0","id":1,"method":"message/send",
    "params":{"message":{"role":"user","messageId":"m1","kind":"message",
      "metadata":{"skill":"compress"},
      "parts":[{"kind":"data","data":{"text":"...","target_tokens":50}}]}}}'

Name the skill in message.metadata.skill, or send a DataPart shaped { skill, input }. A part that names a skill wins over one that only carries input, so a caller that sends both is never billed for the wrong endpoint. Skill ids are the route path with slashes as dots (/once-key/complete → once-key.complete). A non-standard skills/list method returns the catalogue.

message/send always returns a Message, never a Task. The x402 resumption flow expects the server to hold the original input against a returned taskId, and this service stores nothing between calls — so a Task it could not resume would fail on the second leg. tasks/get therefore answers -32001 honestly and streaming is refused with -32004, rather than advertising operations that would break. An unpaid call still returns the x402 challenge under the a2a-x402 extension's metadata key, so a payment-aware client can sign and retry in one step.

Calls arriving over /a2a re-enter the same route in-process, so they pass the identical pricing, payment, validation and body-cap path as direct HTTP.

MCP Server

Every paid endpoint is also exposed as an MCP tool over Streamable HTTP at https://agentic-endpoints.com/mcp, implementing revision 2026-07-28 (stateless: no initialize handshake, no session header) with a fallback for clients still sending the 2025-06-18 handshake.

json
{
  "mcpServers": {
    "agentic-endpoints": { "type": "http", "url": "https://agentic-endpoints.com/mcp" }
  }
}

tools/list is free so clients can discover the catalogue. tools/call re-enters the corresponding paid route in-process, so it passes the same x402 gate, body cap and validation as a direct HTTP call. Without a valid X-PAYMENT header the tool returns isError: true and a machine-readable payment demand (price, payTo, asset, network) rather than performing work.

Two Ways To Pay

Per-call x402 caps revenue at whatever a buyer will tolerate signing: $1,000 at $0.005 a call is 200,000 signatures. Prepaid credits sell the same work once, in an amount worth the transaction, and let callers whose wallets cannot sign per request use the service at all.

Both paths run side by side and neither is privileged:

bash
# Per call, unchanged
curl -X POST https://agentic-endpoints.com/compress -H "X-PAYMENT: ..." -d '{"text":"..."}'

# Or prepay once, then no signatures
curl -X POST https://agentic-endpoints.com/credits/buy -H "X-PAYMENT: ..."   # -> credit_token
curl -X POST https://agentic-endpoints.com/compress \
  -H "X-Credit-Token: ae_..." -d '{"text":"..."}'

Omitting X-Credit-Token produces exactly the 402 challenge it always did, so the Bazaar listing and every existing integration are unaffected.

Credits are integer micro-dollars, never floats: $0.001 has no exact binary representation, and a ledger that drifts is worse than no ledger. Each account is its own Durable Object addressed by the hash of its token, so the balance check and its debit are atomic and one account cannot queue behind another. Calls are debited before the work and refunded if it 5xxs, because an outage must not bill a customer for nothing.

The token is shown once and is not recoverable — only its hash is stored.

Discovery

An endpoint nobody can find earns nothing, so the service is registered wherever agents actually look. Every catalog below was chosen because it verifies ownership by domain or wallet rather than by a financial account — the Coinbase CDP Bazaar is skipped for exactly that reason.

CatalogStatusHow
PayAI BazaarNot listed on mainnet — mechanism proven on testnetA /verify is enough: it catalogues and moves no funds, and the catalog worker admits a POST route whose read-only probe answers 402. Confirmed on Base Sepolia — 16/16 routes listed with source: "verify" and the signer's balance unchanged. Mainnet needs only that the payer hold ~$0.03: X402_TEST_PRIVATE_KEY=0x... node scripts/list-in-bazaar.mjs
x402-list.comDead — directory shut downSubmitted and domain ownership verified on 2026-09-12 (probe_result endpoints_found=16), review pending. By 2026-09-16 the domain was parked for sale on Spaceship: nameservers launch1/launch2.spaceship.net, no service behind it. The review will never arrive. The ownership token has been deleted and /.well-known/x402list.txt now 404s. Worth recording rather than deleting: an x402 directory folding inside a month is a fact about the market, not just about this listing
Official MCP RegistryPublished — com.oliverkiss/agentic-endpoints, status active./scripts/publish-registry.sh. Ownership proven by an apex TXT record and an ed25519-signed timestamp, so no financial account is involved
npmPublished — agentic-endpointscd sdk && npm publish. Counts as discovery, not just convenience: npm is crawled by every AI coding assistant, so the client is findable by the same models that would use the service
SmitheryPublished — kiss-olie/agentic-endpoints, quality 98/100npx -y @smithery/cli@latest mcp publish "https://agentic-endpoints.com/mcp" -n kiss-olie/agentic-endpoints — no auth needed, and it updates in place. The scan finds all 20 tools (re-publish after adding one, or it keeps reporting the old count). Note the namespace is kiss-olie, not the GitHub handle. Their registry API and the markdown view served to non-browser clients are both badly stale (still report 12 tools and the old favicon); only the browser-rendered page is current, so verify there
GlamaNot listed; claim file pre-placedglama.json at the repo root (maintainers: ["olie-kiss"], schema glama.ai/mcp/schemas/server.json) claims the listing automatically if their GitHub crawler indexes us. The separate /.well-known/glama.json is a different schema (connector.json, an opaque glama_claim_ token) for remote connectors and needs a listing to exist first

Aggregators such as PulseMCP ingest from the official registry, so publishing there covers several directories at once. The repository is public, so directories that crawl source repos can now see it too.

Machine-readable descriptions are generated from the same pricing table that gates payment, so they cannot drift from what is actually charged: /llms.txt for a model handed a bare URL, /openapi.json for tooling, the A2A Agent Card at /.well-known/agent-card.json, plus /robots.txt and /sitemap.xml. Tests assert the prices agree across all of them, and that the card advertises exactly the skills /a2a can actually run.

Policies

A buyer — or the underwriter behind one — checks for these before sending money or data, and their absence is itself a reason to pass. /terms, /privacy, /refunds, /acceptable-use and /compliance are served as HTML, and /.well-known/compliance.json serves the same content machine-readably. Both render from one source, so they cannot disagree.

They are written to be true of the code rather than boilerplate, and two tests exist purely to stop them quietly becoming lies: one asserts no client IP is recorded anywhere in /stats, and one asserts the free trial the refund policy points at still works.

The support address is set with wrangler secret put SUPPORT_EMAIL rather than committed. It is printed on public pages either way, so this is not secrecy — it keeps the address out of a public repository's permanent history, which harvesters scrape far harder than they crawl a site. A malformed value falls back to the issue tracker instead of publishing a mailto: link that silently goes nowhere.

Revenue Monitoring

The service could demand payment for months with no way to tell whether a payment ever arrived — including the failure mode where payments verify but never settle. A cron trigger sweeps Base every 5 minutes for USDC Transfer logs into the receiving address and folds them into a running ledger in KV.

Revenue is read from the chain, not from our own logs or the facilitator's word, so it cannot be inflated by a bug on either side. GET /revenue publishes the ledger for free — it costs nothing and gives a prospective caller evidence the service actually transacts.

Set ALERT_WEBHOOK_URL to a Discord webhook to be notified when money lands:

bash
npx wrangler secret put ALERT_WEBHOOK_URL

The first scan starts the watermark at the current chain head rather than genesis; scanning millions of blocks through a public RPC node would fail repeatedly and never establish a watermark at all. The watermark advances only on a successful scan, so a transient RPC failure is retried on the next tick with nothing missed.

It is gross inflow, not accounting revenue. Every USDC transfer into the address counts, including your own top-ups and anything else that is not a sale. When the RPC node refuses eth_getLogs (public nodes often do from Worker IPs), the sweep falls back to the net balance change, which misses a payment that lands in the same five minutes as a withdrawal. Set a log-serving BASE_RPC_URL to avoid that, and use a block-explorer export, not this ledger, for tax records. The lifetime total is kept in exact base units (totalMicros), never accumulated as a float.

Sanctions screening

Anyone can pay, and nothing identifies them, so the payer's address is checked before any money is taken. Every paid request's x402 payment names its payer inside the signed authorization; that address is looked up in Chainalysis's free on-chain sanctions oracle on Base (0x3A91A31cB3dC49b4db9Ce721F50a9D076c8D739B) before verification or settlement. A listed payer gets 451 Unavailable For Legal Reasons, which, being a 4xx, also guarantees the middleware settles nothing. Results are cached per isolate for an hour.

It fails open: if no RPC node answers, the payment proceeds and the failure is logged, because the alternative is that an RPC outage stops every sale. The revenue sweep screens each sender again when funds arrive, which also covers USDC sent to the address directly, outside any request. A hit on either path is posted to ALERT_WEBHOOK_URL.

This is a good-faith control, not a legal determination. The oracle tracks designations led by the US lists, which may not match Canada's in every case.

Buyer signals

Revenue monitoring only sees money that arrived. The service takes thousands of requests a day and, so far, no revenue — and reviewing the callers, almost all of it is liveness probes, trust scanners and directory crawlers, which are indistinguishable from a customer in an access log. The gap that leaves is a genuine buyer being refused for a fixable reason and leaving without ever appearing as a distinguishable line.

src/lib/tripwire.ts emits one structured line per notable request, and the same signals are persisted (see below), so the durable answer is:

bash
curl -s https://agentic-endpoints.com/stats | jq .buyer_signals

To watch them arrive live, or to search the raw log lines:

bash
npx wrangler tail --format json | grep BUYER_SIGNAL

Searching Workers Logs after the fact needs a full-text needle, not a $metadata.message filter: the line is a JSON blob, and the message filter does not match inside it — it silently returns zero rather than erroring, which reads exactly like "no buyer has ever appeared".

SignalMeaningConfidence
payment_attemptCarried a payment authorizationhigh
credit_useAn existing customer spending prepaid credithigh
prospect_402An unrecognised caller hit a priced routelow

It keys on behaviour, not identity. A monitor never carries a payment authorization, so anything that does is trying to buy — and that holds even when the User-Agent claims to be a bot. User-Agent only suppresses known noise and grants nothing, because it is forged for free.

Note that the SDK inherits Node's User-Agent, which is exactly what an anonymous script sends, so no string positively identifies a customer. Anything unrecognised stays unclassified rather than being written off; the tests pin this, since misfiling it would suppress the one signal worth having. prospect_402 is a guess and is marked low confidence for that reason — it should never be read as a sale.

The check runs ahead of the credit and x402 gates, both of which answer without reaching application code, because a refusal is precisely the event worth recording. Payment and credit headers are recorded only as present or absent, never their values. The caller IP is dropped in favour of the country and network name Cloudflare already derived, and the Referer is cut to origin and path, so the query strings where emails and session tokens ride are never stored or published on /stats.

Signals are also persisted in the stats Durable Object, because a log line is only readable while something is tailing it — a first customer arriving overnight would otherwise leave a line nobody read, which would then age out. /stats publishes each event's time, path, signal and outcome; the User-Agent, country, network and referrer describe a customer, so they are served only at /admin/stats, behind ADMIN_TOKEN. The event ring is bounded by count rather than age: the first payment attempt could be the only one for months.

Note that buyer_signals.payment_attempt starts at 1 from a deployment verification probe on 2026-09-09 (ua: persistence-verify/1.0). It could not be removed without exposing a mutation endpoint. The first genuine buyer is entry two.

Incidents

Failures that used to reach only the Worker log are also kept in the stats Durable Object and listed under incidents on /admin/stats (never on the public /stats, since an error message can describe the request behind it):

KindRecorded when
exceptionA route throws; the message is kept
server_errorAny other 5xx, including deliberate 503s
refund_failedA credit refund did not land; the amount and ledger key are kept so it can be replayed by hand
facilitator_unreachableThe payment facilitator could not be reached

The first incident of each kind in an hour is also posted to ALERT_WEBHOOK_URL when it is set; repeats are recorded without alerting, so one fault is one message. The newest 200 are kept. Details pass through the same URL redaction as /revenue.

Published SLOs

An agent choosing between two paid services has no way to tell which one works. GET /status is free and answers that from recorded behaviour, not a promise:

json
{
  "uptime_24h": "100.00%",
  "uptime_window": "0.5h",
  "cron_ticks_24h": 6,
  "requests_48h": 88,
  "error_rate_48h": "0.00%",
  "unknown_path_requests_48h": 10,
  "latency_ms": { "p50_at_most": 25, "p95_at_most": 500, "p99_at_most": 500 }
}

Getting the number honest mattered more than getting it published. It first shipped reporting an 11.39% error rate; every one of those was a 404 on a path that never existed — my own probes and passing crawlers — plus tokens that were correctly rejected. An agent reading that would have taken its money elsewhere and been right to. So client errors (4xx) are counted separately from failures (5xx), and requests to unknown paths are excluded from the rate entirely and surfaced as a raw count instead. Otherwise any stranger could degrade our published reliability just by scanning for /wp-admin.

Latency comes from histogram buckets, so the figures are reported as p95_at_most — a bound, which is what a bucket can honestly support, rather than a precise percentile it cannot. Uptime credits only the window actually observed, so day one does not claim 24 hours from an hour of heartbeats. The heartbeat is written before the revenue scan, so an outage at a public RPC node is not reported as ours.

GET /stats publishes the demand funnel — challenged, paid, free, per route — which is the only thing that distinguishes "nobody has found us" from "agents arrive and decline to pay".

Client SDK

sdk/ is a dependency-free TypeScript client. It deliberately does not sign payments — it takes a credit token, or your own x402-aware fetch, so it never needs a private key.

bash
npm install agentic-endpoints

Its reason to exist is exactlyOnce, which collapses the claim/complete/release protocol into one call: it handles all four claim outcomes, records the result so later callers can replay it, releases the claim if your work throws, and rethrows your error untouched.

Stack

  • Runtime: Cloudflare Workers + Durable Objects
  • Payments: x402 protocol (USDC on Base mainnet, exact scheme)
  • State: Durable Object SQLite (OnceKey, Vault, Credits, MeetingMemory, Stats, EndpointRegistry) and KV for the revenue ledger
  • AI: Workers AI, for /meetings/summarize
  • Framework: Hono

Setup

Prerequisites

  • Node.js 20+
  • Cloudflare account (Workers Paid plan — $5/mo, required for Durable Objects)
  • A wallet address on Base to receive USDC
  • Wrangler CLI (use npx wrangler if not installed globally)

Install

bash
npm install

Configure Secrets

bash
# Required — your receiving wallet address (Base USDC)
npx wrangler secret put X402_PAY_TO

# Required — HMAC key for signing JSON receipts
npx wrangler secret put RECEIPT_SECRET

# Optional — support address printed on the policy pages. A secret rather
# than a var so it never enters this repository's public history. Left
# unset, the policy pages point at the GitHub issue tracker instead.
npx wrangler secret put SUPPORT_EMAIL

# Optional — 32+ character bearer secret for /admin/takedown and
# /admin/stats. Unset, both answer 404 and there is no way to act on an
# abuse report.
npx wrangler secret put ADMIN_TOKEN

# Optional — new free trials per UTC day (default 500).
npx wrangler secret put TRIAL_DAILY_CAP

# Optional — enables card checkout of the $69 reachability report at
# /reach/report/buy. Unset, that page still works but only offers x402 and
# prepaid credit; nothing about the rest of the service depends on this.
npx wrangler secret put STRIPE_SECRET_KEY

# Required if STRIPE_SECRET_KEY is set — signs the webhook that starts a
# card-paid report for a buyer who closes the tab before Stripe redirects
# them back. Create the webhook endpoint in the Stripe dashboard pointing at
# https://<your-domain>/reach/report/stripe-webhook for the
# checkout.session.completed event, then set its signing secret here.
npx wrangler secret put STRIPE_WEBHOOK_SECRET

Do not set FACILITATOR_URL unless you mean to override the default. It must be a valid URL. If it is set to anything else, every paid endpoint returns 500 instead of a 402, and the failure is only visible in npx wrangler tail — this silently broke all payments once already.

The default facilitator is https://facilitator.payai.network: no signup, it supports Base mainnet, and unlike xpay.sh it honours the Bazaar discovery extension. Free for the first 1,000 settlements, then $0.001 each.

Develop and deploy

bash
npm run dev
npm run deploy

# Tests run against the real Workers runtime via workers-pool.
# The Durable Object tests call the objects directly, since every paid
# route answers 402 without a real on-chain payment.
npm test
npm run typecheck

# Announce every route to the PayAI Bazaar. Spends nothing: it verifies rather
# than settles, so the wallet only has to hold the prices, never pay them.
export X402_TEST_PRIVATE_KEY=0x...
node scripts/list-in-bazaar.mjs

# Make one real paid call. Requires a THROWAWAY wallet holding a little USDC
# on Base; the key is read from the environment and never written anywhere.
export X402_TEST_PRIVATE_KEY=0x...
node scripts/paid-test.mjs /compress

Proving settlement without spending

Settlement is the one step that cannot be tested by inspection, and on mainnet every attempt costs real USDC. [env.testnet] deploys the same code to testnet.agentic-endpoints.com, priced in Base Sepolia USDC, which faucet.circle.com gives away with no account.

bash
npx wrangler deploy --env testnet
X402_TEST_PRIVATE_KEY=0x... node scripts/paid-test.mjs /once-key \
  https://testnet.agentic-endpoints.com --testnet

Only the exact string eip155:84532 selects Sepolia; anything unrecognised falls back to mainnet. That asymmetry is deliberate — a Worker that wrongly demanded testnet tokens would hand out real work for money anyone can mint.

Announcing to the Bazaar

bash
X402_TEST_PRIVATE_KEY=0x... node scripts/list-in-bazaar.mjs

Lists every utility route in the PayAI Bazaar without spending anything.

Cataloguing runs on the facilitator's /verify as well as /settle, and verification moves no funds. A first listing of a POST resource seen only through /verify is normally deferred until it settles, but the catalog worker admits it anyway when its own read-only probe (HEAD, then GET) answers 402 — which every paid route here does. So the signing wallet only has to hold the price of each route, because verification checks the balance; it never moves it. Three cents covers all 16 routes.

Verified against the live facilitator on Base Sepolia: 16/16 routes went from having no catalog row at all to lastWrite: { status: "listed", source: "verify" }, with the signer's balance unchanged to the last micro-dollar afterwards.

Check any route's status, and the reason if it is missing, with:

bash
curl "https://facilitator.payai.network/discovery/listing-status?resource=\
https%3A%2F%2Fagentic-endpoints.com%2Fcompress"

API Examples

OnceKey (exactly-once execution)

A claim on its own is only half an idempotency key. The agent that loses the race needs to know what happened, or it has to either block forever or repeat the side effect anyway — which is the failure this endpoint exists to prevent. So the lifecycle is three calls, and only the first one costs money.

json
POST /once-key
{
  "namespace": "payment-webhooks",
  "action_key": "stripe_evt_abc123",
  "payload_sha256": "e3b0c44298fc...",
  "ttl": 86400,
  "lease_ttl": 300
}
statusMeaning
claimedYou won. Do the work, then call /once-key/complete
duplicateAlready done. result holds the original outcome — use it. has_result is false if the original caller recorded no result, in which case result is null because there is nothing to replay
heldClaimed by someone who set no lease and never finished. There is no result and may never be one. Do not do the work and do not treat it as done; locked until expires_at
in_progressAnother caller holds a live lease. Wait retry_after; do not do the work
conflictSame key, different payload hash. Your key derivation is wrong; never retry
json
POST /once-key/complete          // free
{ "namespace": "...", "action_key": "...", "namespace_token": "...",
  "result": { "charge_id": "ch_abc" } }

Every later claim of that key returns duplicate with that result.

duplicate always means completed. A claim that was started but never completed reports held instead, precisely so a caller cannot mistake work that is still in flight — or that died half way through — for work that succeeded. The SDK raises HeldError rather than returning a result of undefined.

If the work fails, POST /once-key/release (free) frees the key immediately.

lease_ttl is opt-in, deliberately. Without it a claim is held for its full ttl and nothing can ever run your side effect twice. With it, a claimant that crashes is presumed dead once the lease lapses and the next caller takes over with recovered: true. Leases on by default would have made every key claimed by the original claim-only API silently reclaimable — a duplicated charge is a far worse failure than a key that needs a retry under a fresh name.

The agentic-endpoints npm package wraps all of this in one call:

ts
const { outcome, result } = await client.exactlyOnce(
  { namespace: "billing-4f9c2b1e8d7a", actionKey: `charge:${order.id}`, leaseTtl: 300 },
  async () => stripe.charges.create({ amount: order.total }),
);

Web scraper

json
POST /scrape
{ "url": "https://example.com/article", "format": "text" }

PDF parser

json
POST /pdf-parse
{ "url": "https://example.com/document.pdf", "pages": [1, 2] }

Inflates FlateDecode content streams, expands PDF 1.5+ object streams, and maps character codes through each font's /ToUnicode CMap, so subset and composite fonts come back as real text rather than glyph indices. Encrypted PDFs and image-only scans return 422 rather than filler content, so callers are not billed for a result that is known to be useless.

Token compressor

json
POST /compress
{ "text": "Your very long text here...", "target_tokens": 500, "strategy": "extractive" }

Meeting memory (agent-queryable transcripts)

Meeting notetakers keep transcripts inside their own app, where the only reader is a human scrolling a sidebar. This puts them somewhere your agents can ask questions of them.

You choose, per meeting, whether this service can read it. The choice is required, and the wrong field for the mode is refused rather than guessed.

visibilityYou sendStored asSearchableWe can read it
private (default)ciphertextopaque bytesnono
queryabletranscriptplaintext + FTS indexyesyes

Sending plaintext as private is an error rather than a quiet indexing, and sending ciphertext as queryable is an error rather than a meeting that can never match a search. Both refusals exist because the failure they prevent is silent and only discovered long after it matters.

bash
# First import claims the namespace and returns a one-time token.
curl -X POST https://agentic-endpoints.com/meetings/import \
  -H "Content-Type: application/json" \
  -d '{
    "namespace": "my-meetings-4f9c2b1e8d7a",
    "title": "Pricing review",
    "occurred_at": "2026-09-01T15:00:00.000Z",
    "source": "webvtt",
    "visibility": "queryable",
    "participants": ["Alice", "Bob"],
    "transcript": "Alice: we agreed to ship the redesign before the audit."
  }'

# Then ask a question of every meeting at once.
curl -X POST https://agentic-endpoints.com/meetings/search \
  -H "Content-Type: application/json" \
  -d '{
    "namespace": "my-meetings-4f9c2b1e8d7a",
    "namespace_token": "<from the first import>",
    "query": "pricing"
  }'

Search returns ranked excerpts plus searched_meetings and private_meetings_skipped. Read them. If searched_meetings is 0, an empty result means nothing was searched, not that the topic was never discussed — and an agent that conflates those will confidently tell a user something never happened.

Via MCP the same thing is meetings_search, meetings_import, meetings_get and meetings_list.

Asking a question instead of reading excerpts

/meetings/search hands back ranked excerpts and leaves the reasoning to you. /meetings/summarize takes a plain-language question, retrieves the relevant transcripts, and answers from them with a citation per claim:

bash
curl -X POST https://agentic-endpoints.com/meetings/summarize \
  -H "Content-Type: application/json" \
  -d '{
    "namespace": "my-meetings-4f9c2b1e8d7a",
    "namespace_token": "<from the first import>",
    "question": "what did we decide about pricing?"
  }'
json
{
  "status": "ok",
  "answer": "You agreed to defer the pricing change until Q3 [6f1c3b90-…].",
  "consulted": [
    { "meeting_id": "6f1c3b90-…", "title": "Pricing review", "chars_used": 4820, "truncated": false }
  ],
  "terms": ["decide", "pricing"],
  "ai_generated": true,
  "searched_meetings": 12,
  "private_meetings_skipped": 3
}

The question is not FTS5 syntax — the terms are derived from it and returned as terms, so a bad answer can be traced to bad retrieval rather than guessed at. consulted is what the answer was actually built from; a truncated entry means only part of that transcript was read, so its silence on a point is not evidence.

The negative cases matter more than the positive one. A summarizer's dangerous failure is not a wrong answer but a confident one assembled from nothing, because the caller cannot tell the difference afterwards. So:

SituationWhat happens
No meeting matchedno_matches, the model is never called
Every meeting is privateno_matches with searched_meetings: 0 and an explicit warning not to read it as "never discussed"
Question was all stopwordsunusable_question, no answer
Model unreachable503 with answer: null — plus the meetings it already found, so you can read them with meetings_get rather than paying again

Private meetings can never contribute to an answer; private_meetings_skipped says how many were excluded. Answers are generated by @cf/meta/llama-4-scout-17b-16e-instruct on Workers AI, pinned rather than floating — a silently swapped model would change every answer this endpoint has ever given.

Via MCP this is meetings_summarize.

Importing a raw export

transcript accepts a WebVTT or SRT file exactly as Zoom, Teams, Meet or a notetaker emits it — header, cue numbers, timestamps and <v Speaker> spans included. You do not have to write a parser first.

The format is detected from the content, not from source; that field is a hint and is ignored when it disagrees with the file. Timestamps and cue numbering are stripped before indexing, and consecutive cues from the same speaker are merged, because exports split on timing rather than grammar:

00:00:04.120 --> 00:00:07.880   <v Alice>we agreed to ship the redesign
00:00:07.900 --> 00:00:09.400   <v Alice>before the security audit

Indexed cue-by-cue, a search for "redesign before the security audit" matches nothing — the phrase exists in the meeting but not in any one cue. Merged, it matches. Speakers found in the file are added to participants alongside any you declared, so attendees who never spoke are not lost.

A parsed import reports what happened:

json
"parsed": { "format": "webvtt", "cues": 3, "speakers": ["Alice", "Bob"], "indexed_bytes": 71 }

The absence of that field means the text was stored verbatim. Plain text is unchanged, and private meetings are never parsed — the body is ciphertext this service cannot read.

Checking an endpoint before paying it

An agent that pays automatically cannot notice that the money started going somewhere else. Every response still returns 200, the price still looks right, and by the time a human looks it has happened a thousand times.

bash
curl -X POST https://agentic-endpoints.com/x402/verify \
  -H "X-PAYMENT: <signed payload>" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://example.com/api/thing",
        "expect": { "pay_to": "0xabc...", "max_price_usd": 0.01 }
      }'

It fetches the endpoint's live payment challenge and compares it against every observation made by every previous caller. That comparison is the part an agent cannot do for itself — it can remember what it saw, not what everyone else saw.

json
{
  "status": "ok",
  "charges": {
    "pay_to": "0xabc...",
    "amount": "10000",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "network": "eip155:8453",
    "price_usd": "0.01"
  },
  "first_seen": "2025-01-01T00:00:00.000Z",
  "times_seen": 14,
  "first_observation": false,
  "drift": [
    {
      "field": "pay_to",
      "severity": "critical",
      "from": "0xabc...",
      "to": "0xdef...",
      "note": "The receiving address changed. Payments now go somewhere other than where earlier callers sent them."
    }
  ]
}

A pay_to, network or asset that this endpoint has never offered before is critical — those determine where the money goes. A price change is a warning at worst.

The comparison is over the whole accepts list, not just the first entry. A real client pays through the option matching a chain and token it holds, which need not be index 0, so an endpoint could otherwise list an honest option first and an attacker's payee second and pass a check that only reads the first. Reordering the list is reported as a warning, not a critical, because the same destinations remain on offer.

Addresses are compared case-insensitively. EIP-55 checksumming is presentation, not meaning, and reporting a re-cased address as a changed payee would fire the most severe alarm this tool has at an endpoint where nothing moved.

History is kept per method and URL, not per URL. method is caller-supplied, and plenty of x402 services price GET and POST differently — a read tier and a compute tier. Sharing one history between them would let anyone spend $0.003 to make an honest endpoint appear to the next caller to have swapped its payee, and alternating the two would flap that baseline indefinitely. The URL is canonicalised first (host lowercased, default port dropped, fragment removed) so that different spellings of one endpoint still share one history rather than fragmenting it.

What it does not tell you

ReadingWhat it actually means
"status": "ok"A challenge was read and recorded. Not that the operator will deliver anything.
"drift": [] with "first_observation": trueThere is no history to compare against. Absence of drift is not evidence of stability.
"drift": [] with "prior_criticals": 3This endpoint has changed where money goes before. drift compares against the last observation only, so once a swap becomes the baseline it stops appearing there. Read prior_criticals and last_critical before concluding anything from an empty drift.
"status": "unreachable"Neither evidence of fraud nor of health. Nothing is recorded, so one timeout cannot manufacture a payee-change alarm later. drift is null rather than [], because no comparison was made.
"price_usd": nullThe token's decimals are unknown here, so the amount was not converted. Not a claim that it is small.
"status": "not_x402"No challenge was found. The endpoint may be free, may want a different method, or may not use x402 at all.
"status": "refused"The URL was never fetched. It is checked by the same outbound guard /scrape and /pdf-parse use, which resolves the hostname over DNS-over-HTTPS and rejects private answers — so a public name pointed at 127.0.0.1 is refused too.

Network identifiers are normalised before comparison, so an endpoint moving from x402 v1's "base" to v2's "eip155:8453" is correctly read as the same chain rather than as a critical chain change.

Failures are reported coarsely (timeout or unreachable) and a redirect target is never echoed back. Reflecting the real error or the Location header would turn a $0.003 call into a network-probe oracle for any host a caller names.

A critical is recorded permanently, not just reported to whoever happened to call first. Comparing against the previous observation alone would mean an attacker's challenge becomes the baseline after one call, and every caller afterwards sees an empty drift beside a growing times_seen — a record that reads as stability because the change was absorbed, not because nothing happened. prior_criticals and last_critical do not expire when a later look is clean, and the advice says so first.

The word "safe" never appears in a response, deliberately. This reports what an endpoint declares about itself; it cannot certify an operator.

Vault

Storage is free; retrieval is paid. The server only ever sees ciphertext — encrypt client-side before calling.

Namespaces are caller-chosen strings, so they are claimed on first write. That first store returns a namespace_token once; every later operation on the namespace must present it.

json
POST /vault/store
{ "namespace": "my-app-4f9c2b1e8d7a", "key": "secret-1", "ciphertext": "base64...", "ttl": 86400 }

// First write only — save this, it is not shown again
{ "status": "stored", "namespace_token": "30ab4b26..." }
json
POST /vault/retrieve
{ "namespace": "my-app-4f9c2b1e8d7a", "key": "secret-1", "namespace_token": "30ab4b26..." }

Writes are last-write-wins unless you say otherwise, so two agents rotating the same secret would clobber each other silently. Pass if_match with the item's current updated_at for a compare-and-swap, or if_absent to create only; either answers status: "precondition_failed" instead of overwriting.

POST /vault/list ($0.001) returns the keys and their versions but never any ciphertext — that is what the $0.02 retrieve is for.

Rotate a token you think has leaked, with POST /vault/rotate-token. It is free: putting a price on the correct response to a suspected leak is how you get callers who never rotate. It requires the current token, and there is no recovery if that is lost — any path that could restore access without it would be a second way in, and would serve an attacker just as readily as the owner. Lose it and the namespace is gone by design; there is no account to reset it against.

Limits

LimitValue
Free requests (no X-PAYMENT)60/min per IP, per Cloudflare location
Vault writes20/min per IP
Paid requests600/min
Request body2 MiB
Vault item256 KiB ciphertext
Vault namespace1,000 items / 25 MiB

Paid requests are throttled far more loosely than anonymous ones, because a caller who is paying per call already has a spend ceiling. They are not unlimited.

Concurrent paid calls

Paid calls issued concurrently from the same wallet are refused a noticeable fraction of the time — measured at 2 in 10 up to 8 in 15 on Base Sepolia, independent of our rate limits, and reproducible with as few as 5 in flight. The refusal is an ordinary 402, and it originates at the facilitator, which will not verify overlapping authorizations from one payer.

Nothing is charged for a refused call. Measured directly: 10 concurrent calls at $0.005, 8 settled, 2 refused, and the payer's balance moved by exactly $0.040. Any status at or above 400 cancels x402 settlement, and that holds here.

So a 402 on a paid request means the payment did not happen and it is safe to retry with a fresh signature. If you need throughput, issue paid calls sequentially, or retry on 402 with a short backoff. Do not treat a 402 as a charge you need to reconcile.

Since 0.4.0 the SDK does this for you: post() replays a 402 twice by default, with jittered backoff, and maxPaymentRetries tunes it. Jitter is not decoration — the colliding requests belong to a single payer, so a fixed delay would realign them on the next attempt. Retries only happen when you supplied an x402-aware fetch; the SDK never signs payments itself, so without one a 402 is terminal and is surfaced immediately rather than stalling.

Security Notes

  • URL-taking endpoints are SSRF-guarded (src/lib/url-guard.ts): scheme allowlist, private/reserved IPv4 and IPv6 ranges blocked, hostnames resolved over DNS-over-HTTPS and private answers rejected, every redirect hop re-validated, and response bodies bounded. It fails closed. It does not defeat DNS rebinding, and does not claim to: the DoH probe and the fetch are two independent resolutions, so a hostile low-TTL nameserver can answer them differently. Pinning the fetch to the vetted address is not expressible on Workers — cf.resolveOverride only accepts hostnames inside your own zone, and an IP literal breaks TLS SNI. What bounds the risk is the egress path: Workers reach the internet through Cloudflare's network, which has no route to RFC1918 or loopback and exposes no metadata endpoint. Do not place anything sensitive where this Worker's egress can reach it on the assumption that this guard stops it.
  • Vault and OnceKey namespaces are ownership-gated. The first request to a namespace is issued a one-time namespace_token; tokens are stored only as SHA-256 hashes and compared in constant time.
  • New namespaces must be unguessable (16+ characters, mixed character classes). Ownership is first-writer-wins over a global, account-less string and there is deliberately no recovery path, so a short name like invoices or billing could be claimed by anyone for $0.001 and would lock out the rightful owner permanently. Making real namespaces unguessable means there is nothing worth squatting. Use myapp-<uuid>. Names claimed before this rule keep working.
  • The free lifecycle endpoints do not reveal whether a namespace exists. /once-key/complete and /once-key/release return an identical 404 whether the namespace was never claimed or your token is wrong, because a free existence oracle is the reconnaissance step before squatting. The paid /once-key and /vault/* routes answer 200 with status: "forbidden" instead of 403, so that each probe actually settles a payment — a 4xx would cancel settlement and leave the payment header replayable, making the oracle free after all. Branch on status, not on the HTTP code.
  • namespace_token is a bearer credential with no recovery path. Anyone holding it is the owner. Worse than a normal leak: /vault/rotate-token is free and needs only the current token, so whoever steals it can rotate first and lock you out irreversibly. OnceKey has no rotation at all, so a leaked OnceKey token is permanent. There are no accounts, no email, and no support channel that can restore access — treat these tokens like a private key, and store them before you make the call that returns one.
  • The vault cannot read your values, but it does see their names. No key held here can decrypt anything, and plaintext is never received. But the item key, the namespace, the alg label and the size are all stored in the clear, so the service can tell which named secrets you hold and how large they are. alg is an advisory label: nothing here can verify that what you sent was in fact encrypted. Use high-entropy namespace names — ownership is first-writer-wins, so a guessable namespace can be squatted (now enforced; see above).
  • Paid routes answer completed work with 200 and a status field, never a 4xx. The x402 middleware cancels settlement above 399, so a 4xx returned after the work is done gives the answer away free and leaves the payment header replayable.
  • Receipts are HMAC-signed with RECEIPT_SECRET. Note that only /once-key and the vault endpoints return a receipt — the stateless utilities (/pdf-parse, /scrape, /compress) do not.
  • The operator can suspend or delete any vault, meeting or OnceKey namespace through POST /admin/takedown, to act on abuse reports (see Acting on an abuse report). It cannot read vault values, which are encrypted before they arrive, but it can make them unavailable. The route is disabled unless ADMIN_TOKEN is set, and answers 404 to anyone without it.
  • Every 402 challenge links /terms, because paying is what accepts them.
  • The wallet is the trust anchor. Nothing in this codebase protects the seed phrase behind X402_PAY_TO. If it leaks, the money is gone.

Reachability scan

POST /reach with {"url": "https://example.com/"} answers a plain question: does the same request get the same answer from the HTTP clients agents actually use? It sends one GET as each of nine clients — curl, node/undici, Python urllib, Python requests, libwww-perl, a Chrome browser, GPTBot, ClaudeBot and Googlebot — and reports each status, plus:

  • the edge's own refusal when it identifies one (Cloudflare's error code: 1010 is the Browser Integrity Check refusing a User-Agent string)
  • whether a refused crawler is declared in the site's robots.txt (a choice) or not (a shadow)
  • AI crawlers that robots.txt shuts out of the whole site
  • whether /llms.txt is a real text file
  • whether the http-to-https redirect keeps a POST (307/308) or drops it (301/302)

The verdict is pass, shadowed (some clients refused while others got through: the fixable finding), refused or unreachable.

Found on this service first. The scan's motivating case was this API: Cloudflare's Browser Integrity Check refused Python's urllib and libwww-perl with 403 error code: 1010 on every path, paid routes included, before any request reached the Worker, so /stats never counted the buyers it turned away. The fix is a Configuration Rule on agentic-endpoints.com with Browser Integrity Check off.

What it cannot see. It runs in a Cloudflare Worker, which sets the User-Agent but not the TLS fingerprint, and sends from Cloudflare's network. A refusal keyed on the User-Agent string — the common case — is measured faithfully; one keyed on the TLS fingerprint or IP reputation may read differently than from a real client on a server. Every result says so. It also cannot scan its own hostname: a Worker's request to the host it serves skips the Worker and reaches no origin, so every client would read 522. That request is refused with a 400 rather than reported as a scan. A 5xx from any site counts as not reached and can never produce a pass.

The full report ($69). POST /reach/report with {"urls": [...]} (1 to 20 pages, at most 3 hostnames) answers at once with a report_id and a report_url. A Worker invocation cannot make the ~240 requests twenty pages need, so a ReachReport Durable Object scans one page per alarm, ten seconds apart, which also keeps it to six pages a minute against the target. The finished report turns each refusal into a finding with a severity, the likely cause (Cloudflare 1010, 1020 or 1015, a bot challenge page, the origin server's own refusal, an undeclared AI-crawler block, a POST-dropping redirect, a missing /llms.txt), concrete fix steps and a curl that reproduces it, merged across pages and ordered by severity. Three free re-scans within 30 days (/reach/report/rescan) each show what was fixed, what remains and what is new. The report lives at /reach/report/{id} as a page or JSON, readable by anyone holding the id, and is deleted after 90 days. Payment is x402, prepaid credit, or — for a human buyer with no wallet — a card through Stripe Checkout at /reach/report/buy: enter the pages first, then pay on Stripe's own hosted page, so a card number never reaches this service. The order is held unpaid for 24 hours awaiting confirmation and deletes itself automatically if the buyer never completes checkout; nothing is charged either way. Payment is confirmed twice, by whichever happens first — the buyer returning to the success page, which asks Stripe directly rather than trusting the URL, or a signature-verified webhook for a buyer who closes the tab before coming back — and either alone starts the report.

If you see this scanner in your logs. Each request carries X-Scanner: agentic-endpoints reachability scan and, when configured, a From: contact address. It is read-only: one GET per client identity per scan, nothing fuzzed or authenticated, and at most 6 scans a minute against any one host.

Operator access

/admin/stats and /admin/takedown answer only to Authorization: Bearer <ADMIN_TOKEN>. Anything else gets 404 {"error":"Not found"} — the same answer as a path that does not exist, so the admin routes cannot be discovered by probing. A browser address bar cannot send that header, so opening /admin/stats in a browser shows that 404 by design. Read it from a terminal, with the token kept in a file rather than typed into the command line or a URL (URLs land in browser history and in Cloudflare's logs):

bash
curl -s -H "Authorization: Bearer $(cat ~/.agentic-admin-token)" \
  https://agentic-endpoints.com/admin/stats | python3 -m json.tool

It returns the same summary as /stats plus each buyer event's User-Agent, country, network and referrer, and the incidents section. For browser access later, Cloudflare Access in front of /admin/* is the stronger option: nothing reaches the Worker until the operator has logged in.

Acting on an abuse report

The acceptable use policy commits to acknowledging a report within 2 business days and removing unlawful stored content. Namespace tokens belong to customers, so the operator acts through an admin route instead:

bash
curl -X POST https://agentic-endpoints.com/admin/takedown \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
  -d '{"service":"meetings","namespace":"<ns>","mode":"suspend","reason":"report 2026-09-26, <summary>"}'
  • service: vault, meetings or once-key.
  • suspend blocks every request to the namespace with 410 and keeps the data. Do this first. For suspected child sexual abuse material, Canadian law requires reporting to Cybertip.ca and can require the data to be preserved, so do not purge until any duty to preserve has passed.
  • purge deletes everything in the namespace. It stays refused, so the same person cannot claim it again.
  • lift undoes a suspension made in error. A purge cannot be lifted.

Each action writes a TAKEDOWN line to the Worker logs, but those expire. Keep your own record of each report and what you did about it.

Money Flow

Agent pays USDC on Base
  → lands directly in your wallet (X402_PAY_TO)
  → transfer to an exchange that supports Base USDC
  → withdraw to your bank

Canadian-friendly off-ramps: Kraken, Newton, Shakepay, Coinbase. US-only services such as Mercury and Sphere Pay are not an option.

Known Gaps

  • Mainnet revenue is $0.025 from a single payer. Settlement is proven on mainnet: on 2026-09-24 one wallet paid for five /compress calls at $0.005 each, and every call was served. That wallet paid 91 different x402 services in 83 minutes, so it reads as an automated sampler rather than a customer who chose this one.
  • Not in the PayAI Bazaar on mainnet, though the mechanism is now proven rather than assumed. Listing needs a settled payment per route — reaching /verify does nothing, which is why the catalogue held 0 of 28,095 of our routes. On testnet all 9 appeared within seconds. Announcing the mainnet catalogue costs $0.068, not the ~$1 assumed for months.
  • /x402/verify has no observation history yet. Every endpoint it is pointed at will come back "first_observation": true until someone checks the same URL twice, and drift detection is worth nothing until then. The comparison logic is covered by tests, and the parser has been run against this service's own live challenges — which is how the CAIP-2 mismatch was caught: x402 v2 declares eip155:8453 where v1 said base, and comparing those literally would have fired a critical "the settlement chain changed" alarm at any endpoint that merely upgraded protocol version. Network identifiers are now normalised before comparison. What remains untested is the handler against a third-party endpoint, since every x402 service reachable for free is one of ours.
  • /meetings/summarize has never run behind a real payment. The parts have been verified separately rather than end to end: the model, the grounding prompt and its negative controls were exercised against Workers AI directly (it declines when the transcript does not answer, cites both sides when two meetings disagree, and will not conclude from a truncated transcript), and env.AI.run was proven to work from inside a deployed Durable Object using a throwaway Worker running the same model and message shape. Retrieval, bounds and every no-answer path are covered by tests. What has not been observed is one paid call traversing the whole chain, because that needs USDC. The test suite uses a stand-in model, because the vitest pool cannot reliably reach Workers AI: depending on the day, inference there errors or hangs until the test times out, which once turned the suite red for reasons unrelated to the code. The model call has a 30-second timeout in production for the same reason.
  • No evidence of demand. /stats records the funnel precisely so that "nobody has found us" and "agents arrive and refuse to pay" stop looking identical. So far the answer is the first one.
  • The A2A card targets v0.3, not v1.0. v1.0 is a breaking release (PascalCase methods, kind discriminators dropped, supportedInterfaces[]), and deployed clients still speak v0.3, so that is what is served. The earlier objection to publishing a card at all — that v1.0 makes a declared interface owe all 11 operations — is handled by declaring only what is implemented: tasks/get returns -32001 and streaming -32004 rather than pretending. A v1.0 card will be a second document, not an edit to this one.
  • No A2A skill has been invoked by a stranger. The transport is verified end to end against production, including a paid skill run on trial credit, but every caller so far has been us.
  • /scrape, /pdf-parse and /compress compete with free libraries. The defensible endpoints are /once-key and /vault: coordination primitives a single agent cannot self-host, because they answer questions about what other agents have done.
  • OnceKey namespace tokens cannot be rotated (vault's now can).
  • /x402/verify will POST {} to any public URL a caller names. That is how an x402 endpoint is provoked into showing its challenge, and the body is fixed, the SSRF guard applies and redirects are not followed, so the most a caller can do is make this service send one empty POST somewhere public.
  • Sanctions screening fails open when no RPC node answers, rather than stopping every sale. The revenue sweep screens the payer again when the money arrives.
  • The extractive compression strategy is heuristic and unvalidated against real agent workloads.

License

MIT — see LICENSE.

Similar modules

Browse Agent collaboration