The Crypto Port โ MCP server
What crypto YouTube channels said, as a daily board โ coin calls, stances, dated
catalysts โ served over MCP. Free, no API key.
- Live endpoint (streamable HTTP):
https://cryptoport-mcp.cryptoport-port.workers.dev/mcp
- Registry name:
com.thecryptoport/cryptoport-port (official MCP registry, registry.modelcontextprotocol.io)
- Website (the human board): https://thecryptoport.com
Add to your client
Claude Code:
claude mcp add --transport http cryptoport https://cryptoport-mcp.cryptoport-port.workers.dev/mcp
Any MCP client that speaks streamable-HTTP remotes:
{
"mcpServers": {
"cryptoport": {
"type": "streamable-http",
"url": "https://cryptoport-mcp.cryptoport-port.workers.dev/mcp"
}
}
}
On the live endpoint above, all six tools are free โ latest_board, search, coin,
stance_history, catalysts, board_snapshot. That deployment has no payTo, so no payment
middleware is mounted and nothing can be charged; its own /healthz and /.well-known/mcp.json
say so. The prices in the table further down belong to the paid x402 port (Base Sepolia
testnet) and apply only to a deployment configured with a payTo.
Everything served is commentary on public statements by named channels; every payload carries
stale / stale_days / built_from_brief_date โ read them. Not financial advice, not
real-time.
Build context โ the rung-3 machine port
MCP server + x402-payable HTTP endpoints over thecryptoport.com's daily board.
Network: Base Sepolia (eip155:84532) โ testnet, throughout. No mainnet sign-off exists and
none is assumed. Nothing is charged in real money on any network this server talks to.
Master's ruling of record for this build is C+ (2026-08-24): build the paid-port mechanics
now on testnet only; the paid tier itself stays declined and the mainnet switch is the day-60
ruling. The prices below exist so the payment handshake is real, not because anyone is billed.
Runtime owner: Dex. Ward wrote the money, screening and licensing logic; Dex stands the
process up and keeps it alive. See "Handoff to Dex" below.
What it is
| Tool | Price (testnet USDC) | HTTP twin |
|---|
latest_board | free | GET /api/v1/board |
search(q, limit) | free | GET /api/v1/search?q= |
coin(ticker) | 0.005 | GET /api/v1/coin/:ticker |
stance_history(ticker) | 0.01 | GET /api/v1/stance_history/:ticker |
catalysts(window) | 0.01 | GET /api/v1/catalysts?window= |
board_snapshot(date) | 0.02 โ not charged today | GET /api/v1/board_snapshot/:date |
Free meta: GET /healthz, GET /x402/routes (the price table), GET /.well-known/mcp.json.
MCP: POST /mcp (streamable HTTP) and npm run mcp-stdio (stdio).
The MCP tools are thin proxies onto the HTTP twins inside the same process. One code path for
pricing, screening, refusal and the published-field gate, so the two surfaces cannot disagree.
What the paid tier actually sells โ read this before pricing anything
llms.txt says, in the site's own words: "there is no hidden paid tier of the same rows."
That sentence is a constraint on this build, and it holds: every paid payload names the free
static file the same facts live in (upstream). What is priced is form, not exclusivity โ
a parsed dated series instead of an arc string, a windowed catalyst query instead of a full
file. If a future paid tool ever returns a fact the free tier does not, that llms.txt line
becomes false and has to change first. โ flagged to Larry, not decided here.
Run it
npm install
npm run sdn-refresh
node scripts/keygen.js
X402_PAY_TO=0xโฆ npm start
npm test
npm run demo
Data comes from the rung-0 emitter: CRYPTOPORT_DATA_DIR, else
<repo>/../../Documents/Claude/myPKA/infra/cryptoport-site/dist, falling back to staging
when dist has no feed.json. It is read once at boot into memory โ the request path never
touches the filesystem, which is what makes src/worker.js (Cloudflare Workers) a deployment
choice rather than a rewrite.
Claude Code MCP config (stdio)
{ "mcpServers": { "thecryptoport": {
"command": "node",
"args": ["/home/ops/projects/cryptoport-port/src/mcp-stdio.js"],
"env": { "X402_PAY_TO": "0xโฆ" }
} } }
Streamable HTTP instead: point the client at http://localhost:8402/mcp.
Mechanism chosen, and what was rejected
Chosen: x402 v2 middleware in front of the paid routes + the free public testnet facilitator
- USDC on Base Sepolia. No smart contract, no bytecode of any kind. A price, a receive
address and a verifier are the whole mechanism, and fewer contracts is a security property.
| Rejected | Why |
|---|
| A bespoke access/subscription contract | Pure audit surface for something x402 does natively per call. |
| A self-hosted facilitator | Right idea, wrong scale. Buys decentralisation we do not need and adds a service to keep alive. |
| API keys + Stripe | Reintroduces accounts, PII and an operator in the loop; wrong shape for anonymous agents. |
| Any token, subscription NFT or fractionalised instrument | Pre-CLARITY securities exposure for zero benefit. Hard no. |
@x402/paywall (a peer dep of @x402/hono) | Installs 716 packages including WalletConnect, for a browser paywall UI no agent will ever see. @x402/hono works without it; verified. |
Header names are read out of @x402/core's v2 code paths (corrected 2026-09-05 โ the 8/24
build had the v1 pair): request PAYMENT-SIGNATURE (v1 X-PAYMENT still accepted and still
screened), receipt PAYMENT-RESPONSE (X-PAYMENT-RESPONSE read as a fallback only), terms in
PAYMENT-REQUIRED on the 402 (base64 JSON) โ repeated in the JSON body so an agent does not
have to base64-decode a header to learn a price, and handed to MCP callers verbatim in x402.
test/x402.test.js asserts these names against the SDK's own functions. Package generation matters: the scoped @x402/* v2
packages are pinned in package.json and coexist on npm with the v1 x402-hono/x402-next.
No license is signed and no license hash is referenced here. This rung sells access to a
compilation of public statements; there is no geometry, no licensable work product, and no
EIP-712 typed-data license in the design. The license string in every payload is rung 0's own
attribution notice, passed through untouched.
Gates (all three run before anything is served)
- Advisory-input refusal โ
src/inputs.js. Any argument named like portfolio / holdings /
position / allocation / balance / risk profile / cost basis / PnL / leverage / stop-loss /
"should I buy" is refused with 400 and a plain reason, on HTTP and MCP alike, before
payment is even considered. Values are never inspected โ we refuse the shape, we do not read
the data. Every tool description says so.
- OFAC SDN denied-party screening on the payer โ
src/sdn.js. A local check of every
0xโฆ address found anywhere in the decoded X-PAYMENT payload against OFAC's published SDN
digital-currency address list (991 addresses, 124 EVM, published 08/24/2026), fetched
read-only by scripts/sdn-refresh.js from OFAC's own endpoint and vendored as
data/sdn-addresses.json. A listed payer gets 403, is never settled and is never served.
Not KYT: it screens the paying address, it does not trace funds. No KYC, no accounts, no PII
โ collecting identity for a $0.005 data call creates a worse surface than it mitigates.
- B-2 published-field gate โ
src/allowlist.js. quality, counterweight, rank and
presence never leave the process, however they are spelled, and any undeclared key raises
too. The held-back list has one source of truth โ
infra/cryptoport-site/allowlist.py โ and test/allowlist-parity.test.js fails if the two
drift. Upstream rows are projected on load, so a widened emitter cannot silently widen this
surface.
One decision recorded in that gate (2026-09-07), because a silent drop is still a decision.
projectRung0() drops undeclared upstream keys rather than serving them โ correct by default,
but it had been quietly dropping catalyst_refs from every coin file. Those are the D19 pointers
(claim_id + due + the SUBJECT ticker's canon, never the description) to claims that merely
name this coin, so one claim's text is stored and served exactly once. Dropping them made the
coin tool's own advertised sentence false: it promises "the same facts are also free as static
JSON at /coins/<TICKER>.json" while serving strictly less than that file, and an MCP
consumer saw a different coin shape than the origin. Decided: declare, do not drop. Nothing
new leaves the DB โ every key inside a ref is already published, and the descriptions the refs
point at are already served whole by the free catalysts tool. Pinned by two tests in
test/tools.test.js ("catalyst_refs survives the rung-0 projection" and "the coin tool serves the
origin's catalyst_refs, not a narrower shape"). Code reaches a Worker only when THAT Worker is
deployed โ there are two (free cryptoport-mcp, paid cryptoport-x402), each deployed by its
own command, and the daily KV push refreshes the board, never the code. Current state of both:
"Deploy status" under the demand-counter section at the end.
Correction to the message of commit 06e299f: it says an agent that used the descriptor's wrong
days argument had its call rejected. It did not. Nothing validates a call against the
advertised schema on either transport, so days was silently ignored โ verified live 2026-09-07,
{"days":7} returns isError:false with all 40 catalysts (10 past due) and window_days:null,
where {"window":7} returns 2. Silently wrong, not an error the caller could see. The commit
message cannot be edited without a history rewrite; this line is the record.
Proof the gates can fail (the point of having them):
test/sdn.test.js refuses a genuinely OFAC-listed address end-to-end over HTTP, then removes
that one address from the list and asserts the same call is not refused โ so the 403 is
demonstrably coming from the gate and not from somewhere else. test/b2-leak.test.js injects
each held-back column into the upstream JSON and asserts every tool either raises or projects it
away, and that the HTTP surface answers 500 with no leaked value in the body.
Wallet surface
- One receive-only address as
payTo. It signs nothing, holds no approvals, calls no
contract. The server does not need its key and refuses to start if any
*PRIVATE_KEY / *RECOVERY_KEY / MNEMONIC / SEED_PHRASE variable is present in its
environment (test/network-guard.test.js).
- Testnet throwaway EOA, per the
~/projects/cad-on-chain/scripts/keygen.js pattern, with
one change: the key is written straight to .env.sepolia (mode 600) and never printed.
stdout ends up in terminals, transcripts and session logs, and "no private key in a repo, a
note or a log" has no testnet exemption.
- It does not graduate. A mainnet
payTo is a CDP-managed Server Wallet or it does not exist.
- Chain id 84532 is hard-guarded and every mainnet id is denylisted by name.
- Non-custodial by construction: no buyer funds, no buyer keys, no buyer files, no refunds
(settlement precedes delivery), and no file custody, ever.
Mainnet gate list (verbatim from the Lane C spec ยงC7 โ all must be true, none inferred)
- Master's explicit per-deployment mainnet sign-off, quoted verbatim in the deliverable.
- Paid-tier ruling reopened and granted.
- Pax/Master answers on ยงC5 (a)โ(d), especially the investment-adviser question.
payTo is a CDP-managed account; no key material anywhere on disk; the throwaway-EOA
pattern retired.
- SDN denied-party gate demonstrated rejecting a listed address.
- B-2 leak test green in CI, and demonstrated failing when a held-back column is introduced.
- Facilitator switched from
x402.org (testnet-only) to CDP with credentials held outside the
repo.
- Ownership/receipt question settled on a domain that is not Master's.
- No new bytecode โ if that changes, Slither + Foundry fuzz/invariant in CI and a named
external audit before mainnet.
Status against that list today: 5 and 6 are done and demonstrated. 1, 2, 3, 4, 7 and 8 are
untouched. 9 holds โ this build ships no bytecode, so no Slither/Foundry pass and no external
audit is required; if that ever changes, item 9 applies in full.
Handoff to Dex (runtime requirements, not a runtime decision)
- Uptime. It must survive a reboot unattended, with no permanent operator clicks. On the box
that means a systemd unit โ gated live config โ Master. Ward did not create one.
- Secrets. The process needs exactly one variable:
X402_PAY_TO. It needs no key. Hold
it outside any repo. .env.sepolia / .env.buyer are mode-600 and git-ignored and the server
never reads them.
- Cold start. The whole board is read once at boot (~60 JSON files, tens of ms). On Workers,
the same shape comes from KV via
src/worker.js (env.BOARD keys: feed, coins,
catalysts, sdn), pushed by the 07:50 cron. Both Workers (free and paid) are live on the
davitlabs Cloudflare account โ see the two sections at the end.
- Outbound HTTPS to
https://x402.org/facilitator on every paid call (1โ2 subrequests). The
datacenter-egress problem on this box is inbound reputation at YouTube, not outbound HTTPS.
- Data. Point
CRYPTOPORT_DATA_DIR at whatever the 07:50 job emits. The port re-reads only
at boot; restart it after the emit, or add a reload.
- SDN refresh on the same cron:
npm run sdn-refresh (read-only fetch from OFAC, ~30 s).
- Rate limiting on the free tier is an edge concern โ Dex. The paid tier is self-limiting.
- Anchoring stays rung 1 โ Dex. The port consumes anchors and passes them through untouched;
it never re-anchors, re-hashes or forks the passport record. Note: the current
dist build
carries anchor.root = null while staging has real Merkle roots โ the deployed build is
un-anchored. The server prints a warning at boot and serves the empty anchor faithfully. That
is Dex's to fix, not Ward's.
- Registry submission โ DONE. Listed as
com.thecryptoport/cryptoport-port on
registry.modelcontextprotocol.io via HTTP domain proof (root server.json,
scripts/registry-publish.sh).
Cost
Expect ~$0. Free testnet facilitator, free registry listing, testnet USDC, existing hosting,
no contract, no vendor. The only recurring cost that ever appears is CDP's $0.001 per settlement
above 1,000/month at mainnet, which at any realistic volume for this board is zero. Revenue
expectation is also ~$0: at the ecosystem median a thousand paid calls a month is about $28;
at these prices, $5โ10. This lane is a demo of the stack and an option on a rail.
Known limitations and confidence flags
- Bazaar discovery does not work on testnet โ CONFIRMED 2026-08-24. The free
x402.org facilitator's /supported advertises
['builder-code','eip2612GasSponsoring','erc20ApprovalGasSponsoring'] โ no bazaar โ and
GET /facilitator/discovery/resources returns 404. Our 402s carry a valid bazaar
declaration (registry/bazaar-mcp.json, and test/x402.test.js asserts it), but nothing
indexes it. A real Bazaar listing needs the CDP facilitator, which needs an account. This
contradicts the "Bazaar day one" line in the Lane C spec ยงC3 and is a mainnet-gate item.
The demo stops at the 402 โ settled 2026-08-27 locally and 2026-09-05 on the hosted
Worker (three 0.01-USDC settles on Base Sepolia, HTTP and MCP legs; see the paid-Worker
section below). The buyer holds 0.255 test USDC for future runs.
- MCP registry manifest is the root
server.json; it validates against the live registry
schema and is what scripts/registry-publish.sh publishes. (The earlier unused draft at
registry/server.json was removed 2026-08-30.)
board_snapshot is a stub and is deliberately not charged while it is one: no anchored
historical board series exists yet.
- Settlement metadata arrives in the
PAYMENT-RESPONSE header, not the response body,
because the SDK settles after the handler has already written the body. The MCP layer merges
it into the payload as settlement; HTTP callers read the header. Documented rather than
worked around.
stance_history derives from the published arc string. If rung 0 ever stops publishing
arc, this tool has nothing to parse.
- Ward writes no client-facing copy. The
disclaimer sentence in every payload is engineering
boilerplate about what an anchor does and does not prove โ Rhea/Pax to ratify the wording.
Open questions for Pax / Master (flagged, not decided)
Unchanged from the Lane C spec ยงC5: (a) does charging for a summarised feed of named channels'
calls change the attribution / derivative-work posture; (b) is a paid crypto-calls feed exposed
to investment-adviser framing that the free board is not; (c) money-transmission characterization
of receiving USDC for data; (d) may the seven channel names appear in a paid product. None of
these are answered, and this build charges nobody.
Files
src/config.js env + the chain-id guard + the no-keys-in-env rule
src/allowlist.js B-2 gate (mirrors infra/cryptoport-site/allowlist.py)
src/data.js snapshot, freshness, arc parsing, search โ pure, Workers-safe
src/inputs.js advisory-input refusal
src/sdn.js OFAC SDN screening
src/tools.js the one tool table: prices, schemas, descriptions, handlers
(its advertised half is mirrored in the origin's mcp-tools.json โ
test/descriptor-parity.test.js fails if the two ever drift)
src/x402-routes.js route config, 402 body, Bazaar declarations, price table
src/app.js Hono app: gates, paywall, HTTP twins, free meta
src/mcp.js MCP server + streamable-HTTP mount (proxies the HTTP twins)
src/mcp-stdio.js stdio transport entry
src/node-server.js Node entry: disk -> memory at boot
src/worker.js Cloudflare Workers entry (KV-backed; no account created)
src/stats.js demand counter: counts per tool/transport/day in KV (/stats + /healthz.demand only if STATS_PUBLIC=1)
src/evm-keys.js keygen/address, same pattern as cad-on-chain
scripts/ keygen, sdn-refresh, balance, demo, gen-manifests, kv-push,
stats-read (the operator's read-only view of the private counts)
registry/ server.json (MCP registry), bazaar-mcp.json
data/ sdn-addresses.json (vendored OFAC export)
test/ 91 tests
Free MCP server on Cloudflare Workers (deployed 2026-08-27)
Live: https://cryptoport-mcp.cryptoport-port.workers.dev/mcp โ MCP over HTTP,
six tools, free, no key and no account.
Master ruled on 2026-08-27 that the free MCP server ships before the paid x402 port
is hosted: it is the "free front door onto a human budget" from the strategy memo,
it is the only thing that produces the visitor number the day-30 checkpoint needs,
and the descriptor the site publishes at /.well-known/mcp.json had been naming
tools with no server behind them.
Free by construction, not by policy. wrangler.jsonc sets no X402_PAY_TO, so
createApp never mounts the x402 middleware, isCharging() reports price_usdc: "0"
and paid: false on every response, and no tool description quotes a cost. Master's
ruling 4a โ paid tier declined until the day-60 checkpoint โ is therefore enforced by
the absence of a variable rather than by anyone remembering it.
โ Do not add X402_PAY_TO to this Worker. The paid port is a separate deployment
whose hosting was deferred; turning payments on here would flip a declined tier by
editing a config file.
Data path. scripts/kv-push.mjs reads the built dist/ and writes feed,
coins, catalysts and sdn into the BOARD KV namespace. The Worker reads KV; it
never touches a filesystem and never calls back to Bluehost โ which also means machine
clients that Bluehost's mod_security rejects (python-requests gets a 406 there) can
reach the board here.
No zone, no DNS. It answers on the account's own *.workers.dev subdomain. No
record on thecryptoport.com or any other portfolio domain was created or changed, which
is what keeps this clear of the never-touch mail directive on those zones.
Daily refresh โ INSTALLED 2026-08-27 on Master's sign-off
KV holds whatever was last pushed, so the push rides the end of the existing 07:50
chain โ it runs only if the build, the allowlist gate and the deploy all succeeded.
Dry-run it under cron's environment before ever changing this line. wrangler's
shebang is #!/usr/bin/env node, and cron's minimal PATH has no node: the first
attempt died with exit 127, the same trap that killed the channel_intel pipeline on
2026-08-15. kv-push.mjs now extends PATH with its own process.execPath directory.
The check that catches it:
env -i HOME=/home/ops PATH=/usr/bin:/bin SHELL=/bin/sh \
/home/ops/.nvm/versions/node/v24.15.0/bin/node scripts/kv-push.mjs
Operator notes (host, deploy chain) live outside this repo.
Run it by hand any time: node scripts/kv-push.mjs.
Paid x402 port on Cloudflare Workers โ TESTNET (deployed 2026-09-05)
Live: https://cryptoport-x402.cryptoport-port.workers.dev โ the same code as the free
Worker, deployed from wrangler.x402.jsonc with X402_PAY_TO set, so the four priced tools
answer 402 with x402 v2 terms for Base Sepolia (eip155:84532, test USDC) and settle
through the free x402.org facilitator. latest_board and search stay free; board_snapshot
is unpriced while it is a stub. MCP at POST /mcp (same Worker) says COSTS n USDC on Base Sepolia on the priced tools. Nothing is charged in real money; ruling 4a stands. Readout with
the recorded demo, on-chain receipts and the Bazaar staging:
myPKA/Deliverables/2026-08-24-thecryptoport-strategy/12-ward-x402-port-hosted-testnet-2026-09-05.md.
npx wrangler deploy --config wrangler.x402.jsonc
npx wrangler deploy
PORT_URL=https://cryptoport-x402.cryptoport-port.workers.dev npm run demo
Why a second Worker and not an env block: wrangler.jsonc says its absence of
X402_PAY_TO is what enforces the declined paid tier. A paid environment inside that file would
put the declined tier one config line away. Two files, two Workers, no shared switch.
X402_PAY_TO is a plain var โ it is on every 402, so it is not a secret; no wrangler secret exists on either Worker.
Three things the live host taught that the suite could not (all fixed, all tested):
- Do the facilitator handshake yourself, awaited.
@x402/hono starts
httpServer.initialize() at construction and awaits it only on the first paid request.
Workers cancel I/O that is still in flight when a response goes out, so an isolate whose
first request was free hung every paid request forever (wrangler tail: outcome: canceled,
~0 ms CPU, 25 s wall). src/worker.js now passes syncFacilitatorOnStart:false and awaits
app.__x402.initialize() inside build(), caching the isolate only on success.
- The hosted SDN list was empty.
kv-push.mjs read data/sdn.json โ a file that never
existed โ and pushed {"addresses":[]}. Now: the right file, a refusal to push < 100
addresses, a bundled fallback list in worker.js, a fail-closed guard in app.js (payTo
set + empty screener โ refuse to build), and /healthz.sdn so the loaded gate is visible.
- v2 header names. Clients send
PAYMENT-SIGNATURE; the gate read only X-PAYMENT, so a
real buyer was never screened, payer was missing and the PAYMENT-RESPONSE receipt read as
null. Every gate now reads what the middleware reads; test/paid-path.test.js signs a real
v2 payload with an ephemeral in-memory key and settles it through the stub facilitator.
Cosmetic, documented: every isolate build logs x402: Route "โฆ" has an invalid bazaar extension: โฆ Code generation from strings disallowed โ the Bazaar extension validates schemas
with Ajv (new Function), which Workers forbid. Declaration-time only; the 402 still carries
the full bazaar block and settlement is unaffected.
Bazaar + registry โ staged, not submitted. x402.org has no Bazaar (re-verified: no
bazaar in /supported, discovery 404). CDP's facilitator answers 401 unauthenticated, so
a listing needs a CDP account + API key as Worker secrets โ Master's gate. When opened:
X402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402, wrangler secret put CDP_API_KEY_ID / CDP_API_KEY_SECRET, and in app.js
new HTTPFacilitatorClient({ url: cfg.facilitatorUrl, createAuthHeaders }) with CDP's JWT
header builder โ then one paid call echoes the extension and the four routes appear at
GET โฆ/x402/discovery/resources?payTo=0xccebโฆ. registry/server.x402-staged.json is the
0.2.0 registry bump that adds this Worker as a second remote; the published 0.1.1 is untouched.
Demand counter โ BUILT 2026-09-23, NOT DEPLOYED โ counts PRIVATE by default
Why. Until this build nobody had measured tools/call by tool on either Worker, and the only
x402 settles on record were paid by our own buyer wallet. Every "agents will find it" argument for
this port rested on a number that did not exist. src/stats.js is that number โ counts only.
What is stored, and nothing else. One KV value per (Worker, UTC day, isolate) in the existing
BOARD namespace โ no new binding, no wrangler config change:
stats:<cryptoport-mcp|cryptoport-x402>:<YYYY-MM-DD>:<shard>
{ v, date, deployment,
tools: { <tool>: { mcp|http: { calls, errors, http_402, settled, known_bot } } },
tools_list: { mcp: { calls, known_bot } },
excluded: n }
stats:<deployment>:since the first UTC day this build counted anything
No IP, no payer address, no user agent, no arguments, nothing finer than the day. The
caller is read once at the edge (classifyOrigin) into two booleans โ is this the operator's own
egress and does the user-agent family look like a crawler/monitor (known_bot, a coarse regex;
agent runtimes such as node, curl, python-httpx and claude-code deliberately do not match) โ and
the IP and UA go no further. record() builds the stored doc from enumerated fields only, so an
extra field has nowhere to land. test/stats.test.js proves it with sentinel IP / UA / payer /
query values, and a negative control: the same check pointed at a store that does leak must fail.
What counts. calls = every call that reached a tool (MCP tools/call or its HTTP twin, counted
once โ the MCP layer's in-process call is marked so it is never also counted as http);
http_402 = payment terms issued; errors = refused or failed (400 advisory refusal, 403 SDN,
404, 500); settled = paid and settled; served (computed) = the rest. tools_list counts MCP
discovery. Non-tool paths are not counted.
The operator is excluded โ but never in source. This repo is public, so no IP address is ever
committed to it โ not in src/stats.js, not in wrangler.jsonc / wrangler.x402.jsonc (also
committed). DEFAULT_EXCLUDED_IPS is an empty array by design; the exclusion list is read ONLY from
the STATS_EXCLUDE_IPS env var at deploy time (comma list, exact IPs or IPv4 CIDR, e.g. 203.0.113.7
or 203.0.113.0/24). With the var unset, no address is excluded and every call counts. Excluded
calls add only to excluded, never to a tool bucket.
Master sets it per Worker โ preferred as a secret (never appears in wrangler.jsonc, this repo,
or wrangler deployments list):
npx wrangler secret put STATS_EXCLUDE_IPS
npx wrangler secret put STATS_EXCLUDE_IPS --config wrangler.x402.jsonc
or as a plain --var at deploy (visible in deployment metadata, but still never committed to a file):
npx wrangler deploy --var STATS_EXCLUDE_IPS:203.0.113.7 (and the same with --config wrangler.x402.jsonc).
The box's egress moves between datacenter ASNs; the current addresses and their history live in
infra/cryptoport-site/own-egress-ips.txt in myPKA (no remote โ never this repo). Re-read that
file and re-set the var when the egress changes.
KV budget โ this account is on the Workers FREE plan. KV allows 1,000 writes and 1,000 lists
per day account-wide, shared with the 07:50 kv-push (4 writes) and the CAD on Chain Worker. A
per-call write would starve the board refresh at ~1,000 calls. So each isolate owns one key per day
(single writer โ no read-modify-write, no lost increments under KV's ~60 s cross-colo lag) and
flushes its cumulative totals via waitUntil: first ~10 s after an event, then at most every 10 s,
at most every 15 min after 50 writes, hard stop at 150 per isolate per day; a failed write pauses
counting for the rest of the UTC day. Public reads (/stats, /healthz, only with STATS_PUBLIC=1)
share one aggregate per isolate, cached 15 min, with their own list budget; a private Worker spends no
KV list on reads at all. The result is a floor, not an exact count, and every
payload says so. Sustained thousands of calls a day is the signal to move this to a Durable Object
or Workers Paid โ Master's call, and a good problem.
The counts are PRIVATE by default (Master's ruling S3.1 = B, 2026-09-23). Both Workers count
into KV exactly as described above, but with STATS_PUBLIC unset GET /stats is not mounted โ it
answers the same 404 {"error":"not_found",โฆ} as any unknown path, so it does not advertise itself โ
and /healthz carries no demand block (not even enabled:false). Counting is independent of
this switch: STATS_ENABLED alone turns it on or off.
Read it (operator, from the box, read-only):
cd ~/projects/cryptoport-port && node scripts/stats-read.mjs
cd ~/projects/cryptoport-port && node scripts/stats-read.mjs --days 30 --only x402 --json
It lists and gets the stats:* keys from the BOARD namespace named in wrangler.jsonc and
wrangler.x402.jsonc (today both bind the same one) via this repo's own wrangler, always with
--remote, on the box's existing wrangler login โ the same auth path scripts/kv-push.mjs uses. It
prints the aggregate a public /stats would serve (per tool, per transport, per day; --json gives
the exact payload), built by the same mergeShard + summarize in src/stats.js. It never
writes: only kv key list / kv key get with --remote can reach wrangler, and anything else is
refused before a process starts (test/stats-read.test.js). One run costs one KV list per distinct
namespace plus one read per shard. A shard it cannot parse marks the view partial instead of being
dropped.
Making them public is Master's switch, per Worker, at deploy:
npx wrangler deploy --var STATS_PUBLIC:1 (and the same with --config wrangler.x402.jsonc). Only
the exact value 1 publishes. A --var lasts only until the next plain deploy, which makes the
counts private again. With it on, today's public behaviour returns:
curl -s https://cryptoport-mcp.cryptoport-port.workers.dev/stats?days=14 (1..31, default 14) and
/healthz โ .demand (last 7 days). The public payload is B-2-gated like every other response.
Days before counting_since were never measured: they are omitted, not zero. Arrays of
{name, โฆ} only, never maps keyed by tool, so the key-walking B-2 gate sees declared keys only; a
malformed or foreign KV value contributes nothing (tested).
Kill switch, no code change: npx wrangler deploy --var STATS_ENABLED:0 (same for the paid
config). The Node server (npm start) has no KV and counts nothing; with STATS_PUBLIC=1 its
/stats says so.
Cloudflare-side numbers (checked 2026-09-23, read-only). The box's wrangler OAuth grant has no
analytics scope (wrangler whoami lists no analytics or observability read), and wrangler has no
analytics-read command, so request counts from Cloudflare's GraphQL Analytics API need a scoped
API token with Account Analytics: Read โ Master's gate; not attempted. That API would give
per-Worker request counts by status, but never the MCP tool name (it lives in the POST body),
which is why this counter exists at all.
Deploy status (2026-09-23) โ BOTH Workers need a deploy for the counter
| Worker | Live build | Needs |
|---|
cryptoport-mcp (free) | 2026-09-08 03:00 UTC | npx wrangler deploy โ for the counter |
cryptoport-x402 (paid, testnet) | 2026-09-07 03:14 UTC | bash ~/x402.sh go โ for the 9/07 fixes and the counter |
Commit the changed source first: scripts/deploy-x402.sh refuses a dirty src/, wrangler*.jsonc
or package*.json. Both /stats URLs answer 404 before and after the deploy (private default), so
a curl cannot tell the builds apart. Verify with node scripts/stats-read.mjs instead: it reads
counting_since=never until the first outside call reaches a Worker running the counter build.