nano-mcp
This MCP server lets AI agents handle Nano (XNO) payments per API call through one-time addresses, quotes, on-chain verification, and paid tool execution.
Mint or reuse a one-time
nano_payment address for a request (get_address).Read raw balance and recent on-chain history for any
nano_account via publicrpc.nano.to(get_balance,get_history).Price a call in XNO or USD, with USD prices converted via the median of three sources and quotes expiring in ≤30s (
quote,quote_usd).Verify an on-chain send to a request’s one-time address exactly once; repeat verification returns
status='spent'(verify_payment).Run an agent-side pay-and-call flow: given a quote, send payment to the one-time address and then request protected tool execution (
pay_and_call).Underpinned by a pure-Python SDK for wallet derivation, balance/history reads, capped signed sends, and a self-hostable x402 facilitator (
/supported,/verify,/settle) using multi-RPC, exactly-once settlement.Operates as a stdio-only MCP server requiring only
NANO_PAYMENT_MASTER_SECRETto derive per-request payment addresses.
Provides tools for managing Nano (XNO) wallets, generating one-time payment addresses, verifying payments, and enabling pay-per-call API access using Nano transactions via the public node rpc.nano.to, including a self-hostable facilitator for the x402 protocol.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@nano-mcp create a one-time payment address for a $1 API call"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
nano-mcp
An MCP server + SDK so any AI agent can hold XNO (Nano) and pay per API call, transacting through the public node rpc.nano.to (no local node, no issuer, no bridge).
Built by an AI agent (Rai). Tests are run against the live rpc.nano.to node.
Public release — this is the clean, gate-2 release of the nano-mcp project. It is a single-commit, secret-scanned snapshot (no git history, no withdrawn
draft/x402P1 duplicate), published so any agent or API consumer can use thenano_mcpandnano_sdkpackages. The working copy with the full law-ledger history stays private.
5-minute quickstart
The MCP server is stdio-only and needs one secret: a master secret it uses to derive a one-time
payment address per request. Nothing else is required (the default RPC is the public
rpc.nano.to node).
# 1. run it straight from a checkout (uv resolves the deps for you)
NANO_PAYMENT_MASTER_SECRET=$(python3 -c "import os;print(os.urandom(32).hex())") uv run python -m nano_mcp.server
# 2. or install the packages and register the server with an MCP client
uv venv .venv && source .venv/bin/activate
uv pip install -e ".[dev]"
python -m pytest -m "not network" # 472 offline tests collected; 3 journal-plugin modules skip (not in this release)
# 3. installing the package puts the server on PATH as `nano-mcp` -- this is the
# command an MCP client config starts, and the one the registry entry means
# by its `stdio` transport
NANO_PAYMENT_MASTER_SECRET=$(python3 -c "import os;print(os.urandom(32).hex())") nano-mcpnano-mcp starts and lists its tools with no configuration at all, which is what an MCP
directory asks of it; the tools that must derive a payment address refuse until
NANO_PAYMENT_MASTER_SECRET is set, naming the variable. To see that exchange yourself, exactly
as a directory performs it:
python tools/directory_handshake.py -- nano-mcp
# 7 tools: get_address, get_balance, get_history, pay_and_call, quote, quote_usd, verify_payment
# the handshake completed: a directory can start this server and list its toolsVerified public install (pinned to the v0.1.0 tag):
uv pip install "nano-mcp @ git+https://github.com/dhyabi2/nano-mcp-public.git@v0.1.0"
python -c "import nano_mcp, nano_sdk; print('nano_mcp OK')"Use the SDK to derive a wallet and read a balance:
from nano_sdk import RpcClient, Wallet
from nano_sdk.crypto import derive_account
# seed from your env only — never commit it
account = derive_account(SEED, 0)
print(account.address)
wallet = Wallet(seed=SEED, client=RpcClient()) # reads rpc.nano.to
print(wallet.balance_raw()) # integer raw; nano_str() formats it as XNOTo stand up the x402 facilitator and paid tool surface, see nano_mcp/facilitator.py and
nano_mcp/paidtool.py; live tests need NANO_RPC_URL + NANO_RPC_KEY and a funded test
wallet (the on-chain send is written but — honestly — not yet faked, see Status).
Related MCP server: Nerve RPC MCP
Why Nano
Feeless — no per-transaction fee, so truly per-call (even micro) billing is economic; no batching.
Instant — >99.9% of transactions settle in under a second.
Green — no mining.
No issuer — self-custody; nothing to freeze, no trusted third party to verify the payment.
Repo layout
nano_sdk/— pure-Python SDK: derive a wallet from a seed, read balance/history, and (later blocks) sign + publish sends viarpc.nano.to. Crypto is innano_sdk/crypto.py.nano_mcp/— MCP server exposing wallet and pay-per-call tools, plus a self-hostable facilitator (nano_mcp/facilitator.py) exposing the x402exact-on-nano/supported,/verify,/settlesurface (verifies on ≥2 independent RPCs, fails closed).tests/— pytest;nano_sdk/crypto.pyvectors are validated against the live node.
Install / test
uv venv .venv && source .venv/bin/activate
uv pip install -e ".[dev]"
# NANO_RPC_URL + NANO_RPC_KEY must be set for live read/send tests
python -m pytest -m "not network" # offline tests
python -m pytest # includes live rpc.nano.to readsSending and receiving from a wallet (SDK)
from nano_sdk import Wallet
w = Wallet(seed=SEED) # keeps the account's current representative
w = Wallet(seed=SEED, representative="nano_...") # every block re-delegates to this one
block_hash, block = w.send("nano_...", amount_raw)A Nano send and a Nano receive both carry the account's representative, so every block also says who the account delegates its voting weight to.
No representative passed (
representative=None, the default): a send or a receive keeps the representative the node reports for the account. Only an account with none (unopened, or a blank answer) getsDEFAULT_REPRESENTATIVE.A representative passed or assigned (
Wallet(representative=X), orwallet.representative = Xlater): every block signs X, re-delegating the account to it. Any address counts,DEFAULT_REPRESENTATIVEincluded. AssigningNonegoes back to keeping the account's current representative.SessionWallet(buyer SDK) has no representative option: a session send keeps the sub-account's current representative, or the sub-account's own address if it has none.
The RPC node (rpc.nano.to by default) is trusted for the representative just
as it already is for the frontier and balance a block is built from. Its answer
is checked before any proof-of-work is requested: a representative that is not a
string or not a checksum-valid nano_ address raises RpcError, and nothing is
signed or broadcast.
Upgrade note: representatives on send and receive
With the stock RpcClient, earlier versions re-delegated the account on every
block: RpcClient.account_info did not ask the node for the representative, so
the node never sent one, and Wallet.send and Wallet.receive signed
Wallet.representative -- with the default field, DEFAULT_REPRESENTATIVE --
while SessionWallet.send signed the sub-account's own address. Now:
The meaning of an explicit
Wallet(representative=X)changed. The previous release signedinfo.get("representative") or self.representative, so X was a fallback, used only when the node's answer carried no representative. With the stockRpcClientthe node never sent one, so X was in practice signed every time; but a customClientLike, anRpcClientsubclass or a proxy that did return the representative saw the node's value win over X. Now any non-NoneWallet.representativeis signed into every send and receive, re-delegating the account to X even when the node reports a different representative. If you passed X meaning "only for opening new accounts", leaverepresentativeasNone: an account with no representative still getsDEFAULT_REPRESENTATIVE, and every other account keeps its own.Sends and receives keep the account's current representative unless one is passed explicitly. Code that relied on
Wallet()re-pointing accounts atDEFAULT_REPRESENTATIVEshould passrepresentative=DEFAULT_REPRESENTATIVE.Wallet.representativeis nowstr | Noneand defaults toNone. It is kept exactly as given, so a wallet built without one readsNonethere (it used to readDEFAULT_REPRESENTATIVE). Code that read the attribute to learn which representative a block would sign should treatNoneas "the account's current one".RpcClient.account_infonow sends"representative": "true"in the request body, and its result has arepresentativekey (for an opened account). HTTP mocks that match the exactaccount_inforequest body need updating.Exceptions changed:
An invalid explicit representative (for example
""or"bogus") now raisesValueErroron everyWallet.sendandWallet.receive, right after theaccount_inforead and beforework_generateis requested or anything is signed or broadcast.A node answer whose representative is not a string, or not a checksum-valid
nano_address, now raisesRpcErrorbeforework_generateis requested. It used to raiseValueError, afterwork_generatehad already run. Callers that catchValueErroraroundsendorreceiveshould also catchRpcError.
Upgrade notes: payment proofs and the claim store
Claims are case-insensitive on the block hash. One payment settles once whatever hex case its
paymentProofis spelt in. The node is asked for the hash as sent, andtransactionin verify/settle responses echoes it as sent.consumption_key(block_hash, request_id)now returns the hash upper-cased (the request_id is unchanged, and stays case-sensitive).ClaimStore.claimandis_claimednormalise the hash the same way.Upgrade step for a custom
claim_store.settlecalls itsclaim()exactly once, with the canonical key (hash upper-cased), the same call as before except for the key. It never callsis_claimedand never passes the key as sent. Keys your store holds from a previous release are in whatever case the payer sent, so before upgrading add the upper-case form of each one that is not already there (keep the old keys), or a payment settled before the upgrade can be settled once more in upper case. Add only if absent: some keys are already upper case, and two old spellings of one payment share one upper-case key, so a plain insert hits a duplicate and stops part way:import re old = re.compile(r"nano:live ([0-9A-Fa-f]{64})\n? (.*)", re.S) for key in list(my_store_keys()): # every key the store holds m = old.fullmatch(key) if not m: continue new = f"nano:live {m[1].upper()} {m[2]}" if new != key: my_store.claim(new) # add-if-absent; False only means it is thereIn SQL, use
INSERT OR IGNORE(or your database's equivalent) for the new keys.The same applies to a
ClaimStoresubclass that overridesclaim()or does not runClaimStore.__init__(its own backend): it is treated as a custom store.The built-in
ClaimStoremigrates an existingclaims.dbitself. Every open first runs a cheap read-only check for an old-spelling key that lacks its canonical copy; only if one exists does it take the write lock and add the canonical keys (old rows are kept;PRAGMA user_versionis not used). This also covers claims a previous release wrote during a rollback. If the file is read-only, or another writer holds the lock, the open still succeeds as long as the directory holding it is writable (on a read-only mount or directory the open fails, as it did before); the store remembers the migration is pending and retries it before its next claim, and while it still cannot run each claim also looks the key up under any older spelling.Settled through the built-in
ClaimStore(or a subclass that runsClaimStore.__init__and does not overrideclaim()), a proof not spelt in upper case is also stored under the key exactly as sent, so a rollback to the previous release still refuses it. CallingClaimStore.claim()directly stores the canonical key only.Plan the first open after upgrading. The first open of an old
claims.dbmigrates it inside one write transaction, and the time grows with the number of claims: about 1 s per 100,000 and about 13 s for 1,000,000 in our measurements. Disk use for the claims table roughly doubles (one canonical copy per old row; 203 MB became 401 MB for 1,000,000 claims). While it runs, any other process settling against the same file waits the 5 s busy timeout and then fails withdatabase is locked. Stop other writers for that first open, or expect those errors. Later opens only read (about 0.25 s per 100,000 old rows) and take no write lock.Do not run the previous release and this one against the same
claims.dbat the same time (for example during a rolling deploy). A claim the previous release writes after this release opened the file can be settled once more through this release in another spelling, until this release next opens the file. Upgrade every writer together.After a rollback, the previous release behaves as it always did: it refuses the spelling a payment was settled in (this release also stores the key as sent), but it still accepts a spelling neither release wrote, e.g. the same hash in another case.
A
paymentProofwith a trailing newline (or any other character outside the 64 hex digits) is refused.PaidToolServer.execute(paid_tool_execute) returns aninvalidrefusal for a malformed proof instead of raisingValueError.
Address encoding (verified against the node)
PrivK[i] = blake2b-256(seed || uint32be(i)), public key via Ed25519-Blake2b, address =
nano_ + fixed-width-52 big-endian base32 of the public key + fixed-width-8 base32 of the
little-endian blake2b-40(public_key) checksum.
Status
Built and verified under the Law Ledger (.ledger/): blocks 2–15 done.
Block 2: SDK derive + live read (L0, L1).
Block 3: SDK send (sign + PoW + publish) with balance + 0.01 XNO/day cap guards (L3).
Block 4: MCP pay-per-call server — one-time address per request (L4), exactly-once
verify_payment(L5).Block 5: end-to-end probe (L6) + evidence gate that journals a payment as
nano_txonly if it comes from an account we do NOT control (L7).ledger probe→ 88/100.Block 6: dollar-priced quotes — the exact XNO for a USD price from the median of three independent sources, expiring in ≤30s, pure computation (L8, L9).
Block 7: Buyer SDK — owner-signed Ed25519
Mandate+ capped per-session sub-accounts.SessionWalletlets an owner delegate limited, expiring spending authority to an autonomous agent: it verifies the owner signature, the session binding, the per-session cap, the 0.01 XNO/day cap and the balance guard before any block is broadcast, so a compromised agent cannot drain the wallet (L10, L11).Block 12: self-hostable x402
exact-on-nanofacilitator —/supported,/verify,/settleHTTP surface that verifies every payment proof on at least two independent RPC endpoints (fails closed if any cannot confirm) and settles it with an atomic single-use claim, exactly once (L16, L17). This is the real facilitator the x402 spec's "Reference implementations" section describes.Block 13: the multi-RPC verifier parses the real Nano block_info shape (
block_account/contents.type/contents.destination/confirmed:"true") and confirms a real on-chain send on two independent public RPCs (L18).Block 14: the HTTP 402 Resource Server — returns
402 Payment Requiredwith apayment-requiredheader (one-time nano_ payTo + exact amount) and serves the protected result only after the client presents a verified-and-settledpayment-signature(L19, L20). The missing HTTP half of the x402 protocol.Block 15: the MCP paidTool wrapper (
nano_mcp/paidtool.py) —paid_tool_requestissues a one-time nano payTo + exact amount, andpaid_tool_executeverifies the proof on two independent RPCs, settles it exactly once, and returns the protected tool result — the same handshake the HTTP server runs, exposed to MCP agents (L21, L22). Closes the last roadmap-stage-1 deliverable.Block 16: the scorecard reads real evidence. The open rail scorecard's
--journalpath previously accepted only a hand-written JSON array, so its "measured share from nano receipts" could not be computed from the actual evidence store.nano_mcp/journaldb.pyis a stdlib, read-only, network-free adapter that reads the real nano-pulse journal DB (kind=nano_tx, written byappend_nano_tx), andscorecard build/verify --journal-db <path>feeds it straight into the share computation.scorecard/published.jsonnow reproduces exactly from the real DB (strategy law L5/L6): with no external receipts yet it honestly reads 0% share (L23).
L2 (a live funded on-chain send confirmed via rpc.nano.to) is recorded STUCK: no funded test wallet exists, and the money rules forbid seeking funds. Every real component is exercised end-to-end through a chain stub; the live-funded confirmation leg is written but not faked.
Available Tools
7 toolsget_addressA
Return the one-time Nano payment address for request_id (or mint a fresh).
Inputs: request_id. Returns: result
get_balanceA
Return the raw balance of a nano_ account via rpc.nano.to.
Inputs: account (required). Returns: result
get_historyB
Return the last count on-chain entries for a nano_ account.
Inputs: account (required), count.
pay_and_callC
Agent side: given a quoted request_id, pay its one-time address and call
tool after verify. NOTE: broadcast is not executed here in tests (no funded
wallet); this returns the target so a host calling the server can send via the
SDK wallet (Wallet.send) then verify.
Inputs: tool (required), amount_raw (required), request_id (required).
quoteA
Price a call in Nano. Returns {request_id, address, price_raw, price_nano}
where address is the ONE-TIME payment address for this request. Send the
exact amount there, then call verify_payment(request_id, amount_raw).
Inputs: price_nano (required), request_id.
quote_usdA
Price a call in dollars. Converts the USD price to the exact XNO amount
via the MEDIAN of three independent public price sources and returns
{request_id, address, price_raw, price_usd, rate_xno_usd, expires_at}.
The quote expires in <=30s; pay the exact price_raw to address before
that, then call verify_payment(request_id, price_raw). This is pure
computation — nothing is held or converted.
Inputs: price_usd (required), request_id.
verify_paymentA
Service side: approve the call for request_id once an on-chain send of at least amount_raw is confirmed to its one-time address. Approves exactly once; a repeat returns status='spent'.
amount_raw can only make the check STRICTER: the amount actually required
is the higher of it and what this server quoted for request_id.
There is deliberately no require_onchain argument here. The service
method takes one so in-process tests can run without a node, but over the
tool surface the caller is the payer, and a payer that can pass
require_onchain=false is approved with tx_hash "simulated" without any
payment at all.
Inputs: amount_raw (required), request_id (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 7 tools
The quote/quote_usd pair and get_address partially overlap: quote already returns the one-time address, and both quote tools initiate the same request_id lifecycle, so an agent must read descriptions to pick correctly. pay_and_call also overlaps with the explicit quote → pay → verify_payment flow, though the 'Agent side' vs 'Service side' framing in descriptions helps disambiguate. verify_payment, get_balance, and get_history are clearly distinct.
Six of seven tools follow a verb_noun or verb_object pattern (verify_payment, get_address, get_balance, get_history, pay_and_call, quote_usd). The lone bare 'quote' and the noun-like quote_usd break the pattern slightly, but overall the set is predictable and readable.
Seven tools is well-scoped for a Nano payment-gating server: quoting, address issuance, payment verification, an agent-side convenience wrapper, and two account-inspection utilities. No tool feels gratuitous given the two-sided (payer/service) design.
Core lifecycle is covered: quote (XNO and USD) → get address → pay → verify, plus balance/history reads, and verify_payment doubles as a status check since repeats return status='spent'. Minor gaps remain—no explicit quote-status/inspection tool that doesn't consume the request, no cancel/expiry reconciliation, and pay_and_call is stubbed without broadcast—but agents can work around these.
Maintenance
Related MCP Connectors
Pay-per-use tool API for AI agents. Free tier, x402 USDC micropayments, or API key.
Wallet and payments for AI agents: auto-pay x402 APIs in USDC on XDC, within on-chain limits.
Pay-per-use weather, environment, finance, and on-chain intelligence tools for AI agents via x402.
Pay-per-call tools for AI agents: product photos, documents, web pages, data. USDC via x402.
Related MCP Servers
- AlicenseBqualityDmaintenanceEnables AI agents using the Model Context Protocol (MCP) to send Nano cryptocurrency and retrieve account/block information via Nano node RPC.416 npm11MIT
- FlicenseNot gradedqualityDmaintenanceExposes Nerve blockchain JSON-RPC and REST APIs as MCP tools, enabling AI agents to receive, pay, swap, and query balances with local account management and no gas fees.1-
- AlicenseAqualityBmaintenanceSelf-custodied Nano wallet + x402 payment client and merchant server for AI agents, enabling micropayments, on-ledger settlement, and paid endpoints.6283 PyPI3MIT
- AlicenseAqualityBmaintenanceEnables AI agents to check paywalled resource prices, pay and fetch content within enforced budgets, and inspect wallet status.3155 npm3Business Source 1.1