MCP服务器比特币闪电网络微支付服务

Tollbooth DPYC 是一个为 MCP 服务器提供比特币闪电网络微支付的基础设施,通过预付信用额度、工具级按次计费和安全的凭证投递,让 AI 代理按调用付费且不打扰客户。

  • by lonniev
  • Python
  • 本地部署
  • 2 星标
  • 更新于
在 GitHub 查看 lonniev/tollbooth-dpyc

使用说明

Tollbooth DPYC

Don't Pester Your Customer — Bitcoin Lightning micropayments for MCP servers.

Patent Pending — US Provisional Application 64/045,999

The metaphors in this project are drawn with admiration from The Phantom Tollbooth by Norton Juster, illustrated by Jules Feiffer (1961). Milo, Tock, the Tollbooth, Dictionopolis, and Digitopolis are creations of Mr. Juster's extraordinary imagination. We just built the payment infrastructure.


The Problem

Thousands of developers are building MCP servers — services that let AI agents interact with the world. Knowledge graphs, financial data, social media, medical records. Each one is a city on the map. But the turnpike between them? Wide open. No toll collectors. No sustainable economics.

Every MCP operator faces the same question: how do I keep the lights on?

Traditional API keys with monthly billing? You're running a SaaS company now. L402 — Lightning-native pay-per-request? Every single API call requires a payment negotiation. Milo's toy car stops at every intersection to fumble for exact change.

The Solution

Tollbooth DPYC takes a different approach — one that respects everyone's time:

Milo drives up to the tollbooth once, buys a roll of tokens with a single Lightning invoice, and drives. No stops. No negotiations. No per-request friction. The tokens quietly decrement in the background. When the roll runs low, he buys another. The turnpike stays fast.

Prepaid credits over Bitcoin's Lightning Network, gated at the tool level, settled instantly, with no subscription management and no third-party payment processor taking a cut.


Quick Start: Build an Operator in 5 Minutes

The canonical reference operator is tollbooth-sample. Every production operator follows this pattern.

1. Install

pip install "tollbooth-dpyc[nostr]" fastmcp

2. Define Your Tools

from fastmcp import FastMCP
from mcp.types import Icon
from tollbooth.tool_identity import READS_REMOTE, STANDARD_IDENTITIES, ToolIdentity
from tollbooth.runtime import OperatorRuntime, register_standard_tools
from tollbooth.credential_templates import CredentialTemplate, FieldSpec

VERSION = "1.0.0"

mcp = FastMCP(
    "my-weather-service",
    version=VERSION,
    website_url="https://github.com/you/my-weather-service",
    icons=[Icon(src="https://example.com/icon.png", mimeType="image/png")],
    instructions="What this service does, for the agent.",
)

# Domain tools — what your service actually does. The tool_id is a UUID you
# pick once and never change (capability_uuid("...") or uuid4() at the REPL).
GET_WEATHER_UUID = "…"
GET_FORECAST_UUID = "…"

_DOMAIN_TOOLS = [
    ToolIdentity(
        tool_id=GET_WEATHER_UUID, capability="get_weather", category="read",
        intent="Current weather data", title="Current weather",
        behavior=READS_REMOTE,  # reads, and reaches an upstream weather API
    ),
    ToolIdentity(
        tool_id=GET_FORECAST_UUID, capability="get_forecast", category="read",
        intent="5-day forecast", title="Weather forecast", behavior=READS_REMOTE,
    ),
]

TOOL_REGISTRY = {t.tool_id: t for t in _DOMAIN_TOOLS}

Every tool declares a title (its display name) and a behavior — whether it only reads, overwrites or deletes, is safe to repeat, and reaches beyond your own store. Agents read these as MCP tool annotations. tollbooth.tool_identity names the shared ones (READS, READS_REMOTE, WRITES, CREATES_REMOTE, DELETES, …); build a ToolBehavior for anything else. A tool with no ToolIdentity is refused at registration.

3. Create the Runtime

runtime = OperatorRuntime(
    tool_registry={**STANDARD_IDENTITIES, **TOOL_REGISTRY},
    operator_credential_template=CredentialTemplate(
        service="my-operator",
        fields={
            "btcpay_host": FieldSpec(required=True, sensitive=False, description="BTCPay Server URL"),
            "btcpay_api_key": FieldSpec(required=True, sensitive=True, description="BTCPay API key"),
            "btcpay_store_id": FieldSpec(required=True, sensitive=True, description="BTCPay store ID"),
        },
    ),
    service_name="My Weather Service",
    # This service's public URL. Connections prove who they act as here:
    # signed requests (NIP-98) and OAuth holder connections. See Identity & Proofs.
    holder_auth_url="https://my-weather.example.com",
)

Optional — enable field reports (report_issue). No code change required. To let proven patrons file field reports about your tools as GitHub issues on your repo, just deliver two secrets on your operator credential service via Secure Courier:

github_repo   → "owner/repo" you want reports filed to
github_token  → a fine-grained token with issues:write on that repo (no code scope)

The runtime auto-includes these optional fields in your courier template, so no template edit is needed. The <slug>_report_issue tool is always registered; without the two secrets it simply returns an "issue reporting not configured" situation. It is proof-gated and priced (seeds at 1 sat) so it cannot be spammed, and refunds automatically when nothing is filed. The issue's author of record is the caller's npub, stamped into the body.

4. Register Standard Tools + Domain Tools

# This single call registers every standard DPYC tool and returns
# the slug-prefixed @tool decorator for the operator's own paid tools.
tool = register_standard_tools(mcp, "weather", runtime,
    service_name="my-weather-service",
    service_version="1.0.0",
)

from tollbooth.params import PatronNpub

@tool
@runtime.paid_tool(GET_WEATHER_UUID)
async def get_weather(city: str, npub: PatronNpub = "") -> dict:
    """Get current weather for a city.

    Args:
        city: The city to report on.
    """
    # Your domain logic here
    return {"city": city, "temp": 72, "conditions": "sunny"}

5. Deploy

Set one environment variable and deploy:

export TOLLBOOTH_NOSTR_OPERATOR_NSEC=nsec1...
fastmcp run server.py

That's it. The runtime bootstraps everything else from the Authority via encrypted Nostr DM.


How It Works

The Deploy Flow

  1. Set TOLLBOOTH_NOSTR_OPERATOR_NSEC — the operator's Nostr private key. This is the only secret needed at boot.
  2. Runtime bootstraps from Authority — discovers its Neon Postgres URL from an encrypted DM on Nostr relays, signed by the upstream Authority.
  3. Vault initializes — AES-256-GCM encrypted credential and ledger storage on Neon Postgres, schema-isolated per operator (op_{hash}).
  4. Deliver operator credentials via Secure Courier — BTCPay connection details arrive via encrypted Nostr DM (human-in-the-loop, never in env vars).
  5. Configure pricing — set a pricing model via the Pricing Studio iOS app or the set_pricing_model tool.
  6. Serve — patrons purchase credits via Lightning, use tools, credits decrement automatically.

Credit Lifecycle

  1. Patron calls purchase_credits — the operator obtains an Authority certificate (Schnorr-signed Nostr event) and creates a Lightning invoice.
  2. Patron pays the Lightning invoice with any wallet.
  3. Patron calls check_payment — on settlement, credits are added as a tranche with optional expiry (TrancheLifetime).
  4. Patron uses tools — debit_or_deny gates every paid tool call: checks that the connection proves the npub, then constraints and pricing, and debits the balance. FIFO tranche consumption.
  5. Balance runs low — patron buys more credits. No subscription, no interruption.

The Certification Fee Cascade

When a patron purchases credits, the operator's upstream Authority deducts a small certification fee (default 2%, minimum 10 sats) from the operator's pre-funded reserve. The patron always receives exactly the credits they paid for — the fee is the operator's cost of doing business.

Each Authority is itself an operator of its upstream Authority — the same fee cascades up through the chain to the Prime Authority.

Tool Naming: capability vs runtime name

Every tool the wheel registers has three identifiers and exactly one of them crosses the server boundary.

Identifier Example Scope Used for
capability "check_balance" Internal only Python function identifier; seed for the UUID. Lets a slug rename leave Neon pricing rows intact.
mcp_name / runtime name "weather_check_balance" External Wire-exposed tool name, pricing model tool_name, audit logs. The one external identifier.
tool_id UUID5 of capability Internal Neon pricing key. Stable across slug renames.

Use rt.runtime_name("check_balance") whenever you need to refer to a standard tool by name outside this process (e.g., when calling it from another service). Capability names never appear on the wire.


Standard Tools

register_standard_tools(mcp, slug, runtime) registers these tools, prefixed with the operator's slug (e.g., weather_check_balance), and returns the slug-prefixed @tool decorator for the operator's own paid tools. "Operator-only" tools need a connection that proves the operator's npub.

Credit & Billing

Tool Cost Purpose
check_balance Free Current credit balance, tranches, usage summary
purchase_credits Free Create a Lightning invoice for credit purchase
check_payment Free Poll invoice status; credit balance on settlement
check_price Free Preview the effective cost of a tool call (after constraints)
restore_credits Free (operator-only) Credit a patron from a BTCPay-settled invoice that never reached the ledger
account_statement Free 30-day purchase and usage history
account_statement_infographic 1 sat SVG visual of account statement

Identity & Lifecycle

Tool Cost Purpose
service_status Free Operator health, lifecycle state, version info
session_status Free Operator readiness: ready / warming_up / not_registered / no_identity
get_operator_onboarding_status Free Which operator credentials are configured vs missing
get_patron_onboarding_status Free Which patron credentials are configured vs missing
request_echo Free Echo what the client sent (path, headers, cookies, _meta) as SHA-256 fingerprints — nothing sent is repeated back
report_issue Priced (seeds at 1 sat) File a field report as a GitHub issue on the operator's repo (see Quick Start)

Adoption & Recovery

Tool Cost Purpose
request_adoption Free (operator-signed) Ask a chosen Authority to adopt this operator; the Authority's owner approves it
adoption_status Free Status of this operator's adoption request at an Authority
restore_neon_schema Free (operator-only) Re-run this operator's idempotent schema setup when Neon keeps answering 4xx

Secure Courier (credential exchange)

Tool Cost Purpose
request_credential_channel Free Send a credential template DM to a patron or operator
receive_credentials Free Pick up credentials from vault (instant) or relay (DM)
forget_credentials Free Delete vaulted credentials for a service
update_patron_credential Free Add or update a single patron credential field
delete_patron_credential Free Remove a single patron credential field
get_patron_credential_fields Free List stored patron credential field names and delivery timestamps
update_operator_credential Free (operator-only) Add or update one operator secret, preserving the rest of the bundle
delete_operator_credential Free (operator-only) Remove one operator secret (including stored-but-untemplated leftovers), preserving the rest of the bundle

Operator fields are set_once, so before update_operator_credential the only way to apply a reissued secret was to re-deliver the entire bundle over Secure Courier — where any field left out of the reply is a field destroyed. Prefer the field-level tools when rotating or retiring one credential; reserve the full courier flow for first delivery, and forget_credentials only when the whole row should go.

Npub Proof

Tool Cost Purpose
request_npub_proof Free Send a proof challenge DM to a patron; returns the challenge's nonce (holder connections only)
receive_npub_proof Free Verify the patron's signed reply and put the npub in this holder's wallet (holder connections only)
check_proof_status Free check_proof_status(patron_npub) — whether this connection proves the npub, and for how long
held_identities Free The npubs this connection holds, with when each grant expires

Nostr Profile

Tool Cost Purpose
get_nostr_profile Free Read an npub's public kind-0 profile
publish_nostr_profile Free Relay a client-signed kind-0 profile after verifying its signature matches the npub — the wheel never holds a patron nsec

Authority Balance

Tool Cost Purpose
check_authority_balance Free Operator's cert-sat balance at the upstream Authority

Oracle Delegation

Tool Cost Purpose
<slug>_oracle_* Free Delegated calls to the DPYC Oracle: about, how_to_join, get_tax_rate, lookup_member, network_advisory. Mounted under the operator's slug — e.g. schwab_oracle_about, brain_oracle_how_to_join — so every wire-exposed tool on a given operator shares one slug prefix.

Pricing Model

Tool Cost Purpose
get_pricing_model Free Current pricing model with tool registry and constraints
set_pricing_model Free (operator-only) Update the pricing model
reset_pricing_model Free (operator-only) Delete all pricing models and re-initialize
list_constraint_types Free Available constraint types and their parameters
list_canonical_identities Free Canonical (tool_id, mcp_name, category, intent, registered) inventory — source of truth for Reconcile; @paid_tool UUIDs missing from the ToolIdentity table appear with registered: false / in unregistered[]

Coupons

Tool Cost Purpose
mint_coupon Free (operator-only) Create a discount coupon bound to the tools it discounts (tool_ids)
list_coupons Free (operator-only) Every coupon this operator has minted, with redemption counts
update_coupon Free (operator-only) Change a coupon's name, discount, window, caps or bound tools
delete_coupon Free (operator-only) Delete a coupon and its redemptions
redeem_coupon Free Patron claims a coupon by its code; later paid calls apply the discount
list_my_coupons Free The coupons a patron has redeemed, with each one's status
forget_coupon Free Remove a coupon from the patron's redemption list (the coupon itself remains)

OpenTimestamps (when ots_enabled=True)

Tool Cost Purpose
notarize_ledger Free Anchor all ledger state to Bitcoin via Merkle tree + OTS
get_notarization_proof Free Merkle inclusion proof for a specific patron
list_notarizations Free All Bitcoin-anchored ledger snapshots

Conditional Tools

Tool Condition Purpose
request_patron_credentials patron_credential_template set Open Courier channel for patron-specific credentials
receive_patron_credentials patron_credential_template set Pick up patron credentials
begin_oauth oauth_provider set Start OAuth2 authorization flow
check_oauth_status oauth_provider set Complete OAuth2 flow after browser authorization

Authority Extension (tollbooth.authority)

Authority MCPs — the institutions that certify operators' purchase orders — get their own protocol surface that mirrors register_standard_tools for operators. Since v0.22.0, the full Authority tool set lives in the wheel under tollbooth.authority and mounts onto a FastMCP app via one call:

from fastmcp import FastMCP
from tollbooth.authority import (
    AUTHORITY_TOOL_REGISTRY,
    OPERATOR_CREDENTIAL_TEMPLATE,
    register_authority_tools,
)
from tollbooth.runtime import OperatorRuntime, register_standard_tools
from tollbooth.tool_identity import STANDARD_IDENTITIES

mcp = FastMCP("tollbooth-authority-mine", instructions="…")

runtime = OperatorRuntime(
    tool_registry={**STANDARD_IDENTITIES, **AUTHORITY_TOOL_REGISTRY},
    vault_source="env",      # Authority self-provisions its Neon from env
    purchase_mode="auto",    # derive direct/certified from the registry chain
    ots_enabled=True,
    operator_credential_template=OPERATOR_CREDENTIAL_TEMPLATE,
)

register_standard_tools(mcp, "authority", runtime, ...)
register_authority_tools(mcp, runtime)

That's the entire Authority server.py. Everything below is wheel-provided.

What register_authority_tools mounts

Tool Cost Purpose
update_operator Free (operator) Ask to change the operator's registry entry; waits for the owner's approval
deregister_operator Free (operator or owner) Remove an operator from the DPYC community registry — the operator leaving, or the owner removing it
get_operator_config Free (proof-gated) Retrieve the operator's bootstrap config (Neon URL, schema)
operator_status Free Operator registration status + Authority's Nostr npub
certify_credits Ad valorem (default 2%, min 10 sats) Sign a Schnorr-signed Nostr event certificate (kind 30079) for a purchase order
check_dpyc_membership Free Look up an npub in the dpyc-community registry
register_authority_npub Free Step 1 of Authority self-claim — DM challenge to the candidate
confirm_authority_claim Free Step 2 — verify candidate's DM, escalate to parent Authority
check_authority_approval Free Step 3 — confirm parent's approval, activate Authority
receive_adoption_request Free (signed by the operator) Inbound: record an operator's request to be adopted
list_operator_requests Free (owner) Owner queue — pending adoption and update requests
approve_operator_request Free (owner) Approve a pending request: provision an adoption, or apply an update
reject_operator_request Free (owner) Reject a pending adoption or update request
get_adoption_status Free Read the status of an operator's adoption request
repair_operator_schema Free (owner) Reassign all table ownership in an operator's tenant schema to its own role
list_neon_alerts Free (owner) Owner queue — operators that reported a Neon-402 (persistence store locked)
network_persistence_health Free (owner) Proactive per-project Neon quota posture + reactive alerts

Operators join only by adoption: the operator calls its own request_adoption, signed with its key, and the owner approves it in the queue. There is no one-step registration. Each request proves one npub, so anything needing both parties is a request and an approval (docs/deferred-operator-adoption.md). Owner tools take a request that proves the Authority's own npub.

The 3-step Authority onboarding flow escalates to whichever upstream Authority the candidate's dpyc-community registry entry names — Prime for direct-of-Prime, NA for NE, etc. (tollbooth.registry.resolve_my_parent_npub, added v0.20.0).

What the package exports

tollbooth.authority is a thin facade over its supporting modules. The core six were promoted from forked Authority repos in v0.21.0–v0.22.0:

  • onboarding — OnboardingState, OnboardingChallenge, claim/approval credential templates
  • nostr_signing — AuthorityNostrSigner (Schnorr certificate signer)
  • replay — ReplayTracker (anti-replay JTI window)
  • tenant_provisioner — Neon schema + LOGIN-role provisioning
  • role_migration — CLI for migrating legacy schemas to per-operator roles
  • settings — AuthoritySettings (pydantic-settings env reader)

Three more were added as the Authority took on adoption and Neon-quota stewardship:

  • operator_requests — durable queue of operator requests (adoptions and registry updates)
  • neon_admin — Neon control-plane client for the proactive compute/storage-quota watch
  • neon_alert_store — durable store for operator Neon-402 quota alerts

Each runtime singleton (signer, replay tracker, settings, onboarding state) is lazy-initialized on first tool call; Authority MCPs are one-per-process so module-level state is effectively per-Authority.

Architectural note — Authority repos as thin consumers

The three reference Authority deployments (tollbooth-authority, tollbooth-authority-northamerica, tollbooth-authority-newengland) used to fork ~1000 lines of identical Authority code each. As of wheel v0.22.0 their server.py files are ~80 lines of actor-specific configuration: FastMCP name, human-readable instructions, OperatorRuntime construction, and the two register_*_tools calls. Adding a new regional Authority is now a scaffold-and-pin operation, not a fork.


OperatorRuntime

OperatorRuntime is the core protocol engine. All DPYC operations — bootstrap, billing, credentials, pricing, constraints — are delegated to it.

Constructor Parameters

OperatorRuntime(
    # Required
    tool_registry={**STANDARD_IDENTITIES, **YOUR_TOOLS},  # UUID-keyed ToolIdentity map

    # Credential templates
    operator_credential_template=CredentialTemplate(...),   # BTCPay + operator secrets
    patron_credential_template=CredentialTemplate(...),     # Per-patron API keys (optional)
    operator_credential_greeting="Welcome message...",
    patron_credential_greeting="Welcome message...",
    credential_validator=validate_fn,                       # Called on credential receipt

    # Identity & network
    nsec_env_var="TOLLBOOTH_NOSTR_OPERATOR_NSEC",          # Env var name (default)
    service_name="My Service",
    holder_auth_url="https://my-service.example.com",      # Enables signed requests + holder connections
    # Relays come from the DPYC Oracle — not a constructor argument.

    # Billing
    vault_source="authority",     # "authority" (default) or "env" (self-provisioned)
    purchase_mode="certified",    # "certified" (default), "direct", or "auto" (registry-derived)
    operator_settings={},         # Arbitrary operator config dict

    # Constraints
    constraint_gate=None,         # Legacy — use pricing model pipeline instead

    # OpenTimestamps
    ots_enabled=True,
    ots_calendars=["https://a.pool.opentimestamps.org"],

    # Npub proof
    npub_proof_field="confirm",   # Field name in proof DM
    npub_proof_greeting="...",
    on_npub_proven=async_callback, # Called when proof verified

    # OAuth2
    oauth_provider=OAuthProviderConfig(...),  # Enables begin_oauth / check_oauth_status

    # Lifecycle
    on_forget=callback,           # Called when credentials are forgotten
)

Key Methods

Method Returns Purpose
debit_or_deny(tool_id, npub, *, tool_kwargs) int (cost) or dict (denial) Gate a tool call through identity, proof, constraints, pricing, and billing
paid_tool(tool_id) decorator Decorator that wraps debit_or_deny around a tool function
prove_caller(npub) dict | None The one proof gate: does the request in flight prove npub? None on success, else a structured error dict to return verbatim
require_caller_proof(npub) dict | None prove_caller for free / bootstrap standard tools
runtime_name(capability) str Resolve a capability seed to its runtime tool name (<slug>_<capability>) — what crosses every server boundary
mcp_name_for(tool_id) str Resolve a tool UUID to its runtime tool name
vault() NeonVault Bootstraps and returns the Neon vault (lazy)
ledger_cache() LedgerCache Returns the write-behind ledger cache (lazy)
courier() SecureCourierService Returns the Secure Courier (lazy)
operator_npub() str Derives the operator's npub from nsec
resolve_npub(npub) str Validates an npub (bech32 decode)
resolve_expiry() Expiry The operator's credit-expiry decision from the pricing model: unchosen, never, or days
load_credentials(fields) dict Loads operator credentials from the vault
graceful_shutdown() — Flushes caches and closes connections

Pricing & Constraints

Tool Pricing

Every tool has a ToolIdentity that declares its pricing:

ToolIdentity(
    tool_id=GET_WEATHER_UUID,
    capability="get_weather",
    category="read",          # read, write, heavy, free
    intent="Current weather",
    title="Current weather",
    behavior=READS_REMOTE,
    pricing_hint_type="ad_valorem",    # fixed or ad_valorem
    pricing_hint_value=1,              # base cost in sats
    pricing_hint_param="symbols",      # parameter for ad valorem scaling
    pricing_hint_min=1,                # minimum cost
)

The ToolPricing engine computes the final cost: fixed + ceil(percentage * param_value), clamped to [min, max].

Constraint Pipeline

Operators configure constraints in the pricing model's pipeline array. Each step runs on every paid tool call via debit_or_deny. The effective cost (after discounts, free trials, surge) is what the patron pays.

Constraint Purpose
free_trial First N invocations free per patron
happy_hour Time-windowed free/discount with recurrence
surge_pricing Demand-elastic pricing via global counters
temporal_window Time-of-day / day-of-week access windows
finite_supply Total call quotas (per patron or global)
periodic_refresh Rate limiting with ISO-8601 refresh windows
coupon Code-based discounts with expiration — bound by the coupon itself (mint_coupon / update_coupon tool_ids), never authored into a chain
loyalty_discount Spend-based tiered discounts
bulk_bonus Volume bonuses on credit purchases
patron_proof Require each request to be signed with the patron's own nsec (a holder connection does not satisfy it) — for high-value tools
json_expression Custom boolean logic rules

Scoping: Each pipeline step can target specific tools (tool_ids) and/or specific patrons (patron_npubs, max 10). Unscoped steps apply to all tools and patrons.

TrancheLifetime

Credit expiry is a property of money, not a per-tool constraint. TrancheLifetime in the pricing model sets how long purchased credits remain valid. Each purchase creates a tranche; FIFO consumption.


Credential Delivery

All secrets — BTCPay keys, API tokens, OAuth credentials — are delivered via Secure Courier, not environment variables. This is a human-in-the-loop flow using NIP-44 encrypted Nostr DMs:

  1. AI agent calls request_credential_channel(sender_npub=..., service=...), which returns the channel's nonce
  2. Operator/patron receives a DM in their Nostr client with a credential template
  3. They fill in the fields and reply manually
  4. AI agent calls receive_credentials(sender_npub=..., service=..., nonce=...)
  5. Credentials are validated, encrypted (AES-256-GCM with AAD), and stored in the Neon vault

Vault-first lookup means returning patrons activate instantly — no relay I/O needed.

On first relay receipt, the service sends an ncred1... credential card back via DM. Patrons can scan or paste this card to reactivate later.


x402 Upstream Encapsulation

Operators who consume Coinbase x402-protected APIs can absorb the 402 payment ceremony transparently. Patrons never see the 402 handshake — the Operator pays upstream USDC fees as COGS, like server rental.

from tollbooth.x402_client import X402Client

# Wallet credentials delivered via Secure Courier (service "x402-wallet")
creds = await runtime.load_credentials(["wallet_private_key", "wallet_address"])
x402 = X402Client(
    wallet_private_key=creds["wallet_private_key"],
    wallet_address=creds["wallet_address"],
)

@runtime.paid_tool(tool_id)
async def fetch_upstream(query: str, npub: PatronNpub = "") -> dict:
    resp = await x402.get(f"https://x402-api.example.com/data?q={query}")
    return resp.json()  # patron sees data, never sees 402

Per-tool opt-in: only tool handlers that hit x402 upstreams use the client. Requires pip install tollbooth-dpyc[x402]. No refunds, no rebates — the Operator prices their tools to cover upstream costs with margin.


Identity & Proofs

The full description — intention, protocol, lifetimes and usage for agents, web pages, keyed programs and operators — is docs/identity-proofs.md.

Every participant is identified by a Nostr keypair. The npub is your identity in the DPYC Social Contract. The nsec stays with you — never shared, never sent to a service.

Every tool takes the npub it acts as, and only that: the connection proves it. Each request acts as exactly one npub. An action that needs two parties (an operator and its Authority) is a request by one and an approval by the other, never one request carrying both.

A connection proves an npub in one of two ways, both read from the HTTP Authorization header. Setting OperatorRuntime(holder_auth_url="https://<your-host>") turns them on.

Signed requests (a caller that holds the nsec). An Operator calling its Authority, an agent with its own keyring, the native Pricing Studio: each signs every HTTP request with the npub's key, NIP-98 style. The header is Authorization: Nostr <base64 kind-27235 event>, whose tags name the URL (u), the method, the sha256 of the body (payload) and a random nonce. The signature authorizes that one request and nothing else; events older than 60 seconds and replays are refused. The signer needs no grant.

from tollbooth.identity_proof import sign_request
from tollbooth.mcp_client import service_client

header = sign_request(nsec, method="POST", url=url, body=body)   # one request, by hand
client = service_client(url, nsec)                               # a FastMCP client that signs every request

Holder connections (a caller that holds no key). An AI assistant or a web page connects with OAuth 2.1 (dynamic client registration, Authorization Code with PKCE, single-use refresh tokens); the Operator is its own authorization server. Connecting asks the person nothing, because the token names a holder: a connection with no identity of its own. The holder carries a wallet of npubs, each proven once by the patron answering a Secure Courier DM:

  1. request_npub_proof(patron_npub) sends the patron a challenge DM and returns its nonce. The challenge is bound to the holder that asked, so only that holder can collect the reply.
  2. The patron replies from their own Nostr client, optionally stating how long the proof should last; the reply is signed by the patron's key.
  3. receive_npub_proof(patron_npub, nonce) drains the pinned relay, verifies the signed reply, and puts a grant in the holder's wallet. The reply to the caller carries no grant.

The grant is an Operator-signed kind-30080 event binding the patron's npub, the hash of the nonce, the patron's verified reply signature, and the expiry the patron chose (up to a 30-day cap). The wallet keeps it as {"grant": <event>}. On each call the gate checks the Operator's signature, the npub and the expiry; forget_credentials revokes every grant issued to a patron. Many npubs can share one connection, and the grant never enters the model's transcript. held_identities lists what a connection holds, and check_proof_status(patron_npub) reports whether it proves an npub and for how long.

Any other request is anonymous and acts as no npub: free tools answer it, and a paid tool returns proof_required with both ways to prove the npub. request_npub_proof and receive_npub_proof refuse a connection that is not a holder.

A service that keeps no holders, such as the dpyc-oracle, installs tollbooth.holder_auth.SignedRequestProvider: a signed request proves its npub, every other request is anonymous and never refused at the door, and tools read the signer with signed_npub().


Architecture

tollbooth-authority                 tollbooth-dpyc (this wheel)          your-mcp-server
================================   ================================     ================================
Schnorr signing + tax ledger       OperatorRuntime                      OperatorRuntime(tool_registry=...)
certify_purchase -> Nostr cert     register_standard_tools(mcp, ...)    register_standard_tools(mcp, ...)
Authority BTCPay                   debit_or_deny (gate + billing)       @runtime.paid_tool(uuid)
                                   Secure Courier + vault               Domain-specific tools
                                   Pricing resolver + constraints
                                   DPYCRegistry (service discovery)

Dependency flows one way: your-mcp-server --> tollbooth-dpyc. Authority is a network peer, not a code dependency.

Ecosystem Repos

Repo Role
tollbooth-sample Canonical reference operator — start here
tollbooth-authority Tax collection, Schnorr signing, purchase order certification
dpyc-community Membership registry, governance, service discovery
dpyc-oracle Community concierge — onboarding, tax rates, membership
thebrain-mcp Production operator — PersonalBrain knowledge graph
excalibur-mcp Production operator — social media posting
schwab-mcp Production operator — brokerage data with OAuth2
cypher-mcp Production operator — monetized graph answers via named Cypher templates over Neo4j/AuraDB
taxsort-mcp Production operator — tax sorting + classification with Cloudflare Pages UI
optionality-mcp Production operator — options analytics
goodearth-mcp Production operator — region-scoped farm climate analytics
beesknees-mcp Production operator — a multiplayer race to the queen
roastify-mcp Production operator — Roastify coffee catalog and packaging artwork
ChartRemotely-mcp Production operator — relays chart commands to a patron's desktop agent
tollbooth-shortlinks Community utility — URL shortener
tollbooth-oauth2-collector Community utility — OAuth2 callback mailbox
tollbooth-web Shared browser SDK for operator front ends
tollbooth-pricing-studio iOS pricing editor — Nostr DMs, native crypto

Install

# Standard install — includes Nostr relay support (Secure Courier, audit trail)
pip install tollbooth-dpyc[nostr]

# With QR code rendering for credential cards
pip install tollbooth-dpyc[nostr,qr]

Core dependencies (httpx, pynostr) handle identity, proofs, and HTTP. The [nostr] extra adds websocket-client for Nostr relay I/O — required by every real operator since Secure Courier credential delivery, audit publishing, and bootstrap all use relay DMs. The [qr] extra adds segno for credential card QR rendering. The [x402] extra adds x402 + eth-account for transparent Coinbase x402 upstream encapsulation.

# With x402 upstream encapsulation (for operators consuming x402-protected APIs)
pip install tollbooth-dpyc[nostr,x402]

Environment Variables

Variable Required Purpose
TOLLBOOTH_NOSTR_OPERATOR_NSEC Yes The single bootstrap key. Identity, Secure Courier, audit signing.
NEON_DATABASE_URL Trust-root only Neon Postgres URL. Only for vault_source="env" (self-provisioned Authority).

Certified operators (the default) do not set NEON_DATABASE_URL. They discover it from the Authority via encrypted Nostr DM during bootstrap.

The Nostr relay set is not an env var — every server fetches it from the DPYC Oracle (get_relays), which serves relays.json from the dpyc-community repo. It is cached in process for an hour.

All other secrets (BTCPay, API keys, OAuth tokens) flow through Secure Courier — never env vars.


Vault & Persistence

Neon Postgres

NeonVault provides ACID ledger storage with AES-256-GCM encryption (with AAD), optimistic concurrency, and schema-qualified table names via _t().

NeonCredentialVault stores credentials encrypted in schema-isolated Postgres (one op_{hash} schema per operator, provisioned when the Authority approves its adoption).

Schema Isolation

The Authority uses the authority schema. Each certified operator receives an isolated schema (op_{hash}) with a dedicated Postgres LOGIN role. NeonCredentialVault is schema-aware via _t().


Nostr Integration

Audit Trail

NostrAuditPublisher publishes a kind-30078 NIP-78 event on every vault write. Content is NIP-44v2 encrypted — only the patron's nsec can read it. Wrap any vault with AuditedVault to add the trail transparently.

Low-Balance Notifications

NotificationManager sends proactive NIP-44 DMs when a patron's balance crosses thresholds or tranches approach expiration. Fire-and-forget — never blocks the tool path.

Credential Cards

ncred1... bech32-encoded credential cards package encrypted credentials into scannable QR codes. Built on kind-21420 Nostr events with NIP-44v2 encryption.


OpenTimestamps Bitcoin Anchoring

Anchor ledger state to the Bitcoin blockchain for irrefutable, timestamped proofs. MerkleTree builds a SHA-256 tree over all (npub, ledger_json) entries. OTSCalendarClient submits the root hash to public OTS calendar servers (no API key required).


Development

git clone https://github.com/lonniev/tollbooth-dpyc.git
cd tollbooth-dpyc
python -m venv venv
source venv/bin/activate
pip install -e ".[dev,nostr,qr]"
pytest tests/ -q

Further Reading

The Phantom Tollbooth on the Lightning Turnpike — the full story of how we're monetizing the monetization of AI APIs, and then fading to the background.

Trademarks

DPYC, Tollbooth DPYC, and Don't Pester Your Customer are trademarks of Lonnie VanZandt. See the TRADEMARKS.md in the dpyc-community repository for usage guidelines.

License

Apache 2.0 — see LICENSE.


Because in the end, the tollbooth was never the destination. It was always just the beginning of the journey.