Metadata-Version: 2.5
Name: tollbooth-dpyc
Version: 0.105.1
Summary: Don't Pester Your Customer™ — Bitcoin Lightning micropayments for MCP servers
Project-URL: Homepage, https://github.com/lonniev/tollbooth-dpyc
Project-URL: Repository, https://github.com/lonniev/tollbooth-dpyc
Project-URL: Issues, https://github.com/lonniev/tollbooth-dpyc/issues
Author: Lonnie VanZandt
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.12
Requires-Dist: cryptography>=49.0.0
Requires-Dist: httpx==0.28.1
Requires-Dist: pynostr==0.7.0
Provides-Extra: authority
Requires-Dist: pydantic-settings>=2.0; extra == 'authority'
Provides-Extra: constraints
Provides-Extra: dev
Requires-Dist: fastmcp>=3.2.0; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: nostr-sdk<0.46,>=0.45.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=9.0; extra == 'dev'
Provides-Extra: keyring
Requires-Dist: fastmcp>=3.2.0; extra == 'keyring'
Provides-Extra: modal
Requires-Dist: modal>=1.5; extra == 'modal'
Provides-Extra: nostr
Requires-Dist: websocket-client>=1.6.0; extra == 'nostr'
Provides-Extra: ots
Provides-Extra: qr
Requires-Dist: segno>=1.6.0; extra == 'qr'
Provides-Extra: x402
Requires-Dist: eth-account>=0.13.0; extra == 'x402'
Requires-Dist: x402>=2.8.0; extra == 'x402'
Description-Content-Type: text/markdown

# Tollbooth DPYC

[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![PyPI version](https://img.shields.io/pypi/v/tollbooth-dpyc)](https://pypi.org/project/tollbooth-dpyc/)
[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/downloads/)

<p align="center">
  <img src="https://raw.githubusercontent.com/lonniev/tollbooth-dpyc/main/docs/tollbooth-hero.png" alt="Milo drives the Lightning Turnpike — Don't Pester Your Customer" width="800">
</p>

**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](https://modelcontextprotocol.io/) 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](https://github.com/lonniev/tollbooth-sample). Every production operator follows this pattern.

### 1. Install

```bash
pip install "tollbooth-dpyc[nostr]" fastmcp
```

### 2. Define Your Tools

```python
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

```python
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

```python
# This single call registers all 40+ standard DPYC tools 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:

```bash
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:

### Credit & Billing (always registered)

| 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 | Recover credits from a paid invoice lost to cache issues |
| `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 |

### 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 |

### 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 |

### 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 (community info, tax rates, membership). Mounted under the operator's slug since v0.24.1 — e.g. `schwab_oracle_about`, `brain_oracle_how_to_join`. Keeps every wire-exposed tool on a given operator under a single slug prefix. |

### Pricing Model

| Tool | Cost | Purpose |
|------|------|---------|
| `get_pricing_model` | Free | Current pricing model with tool registry and constraints |
| `set_pricing_model` | Free | Update the pricing model (operator-restricted, requires proof) |
| `reset_pricing_model` | Free | Delete all pricing models and re-initialize (operator-restricted) |
| `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[]` |

### 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 |
| `update_patron_credential` | `patron_credential_template` set | Add or update a single patron credential field |
| `delete_patron_credential` | `patron_credential_template` set | Remove a single patron credential field |
| `get_patron_credential_fields` | `patron_credential_template` set | List stored patron credential field names and delivery timestamps |
| `update_operator_credential` | `operator_credential_template` set | Operator-only: add or update one operator secret, preserving the rest of the bundle |
| `delete_operator_credential` | `operator_credential_template` set | 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.

---

## 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:

```python
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](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

```python
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",
    # Relays are drawn from the shared dpyc-community relays.json registry —
    # not a constructor argument.

    # Billing
    vault_source="authority",     # "authority" (default) or "env" (self-provisioned)
    purchase_mode="auto",         # "certified", "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_tranche_lifetime()` | `int \| None` | Reads tranche lifetime from the pricing model |
| `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:

```python
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=...)`
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=...)`
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](https://www.x402.org/)-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.

```python
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](docs/identity-proofs.md).

Every participant is identified by a [Nostr](https://nostr.com/) 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.

```python
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](https://github.com/lonniev/tollbooth-sample) | **Canonical reference operator** — start here |
| [tollbooth-authority](https://github.com/lonniev/tollbooth-authority) | Tax collection, Schnorr signing, purchase order certification |
| [dpyc-community](https://github.com/lonniev/dpyc-community) | Membership registry, governance, service discovery |
| [dpyc-oracle](https://github.com/lonniev/dpyc-oracle) | Community concierge — onboarding, tax rates, membership |
| [thebrain-mcp](https://github.com/lonniev/thebrain-mcp) | Production operator — PersonalBrain knowledge graph |
| [excalibur-mcp](https://github.com/lonniev/excalibur-mcp) | Production operator — social media posting |
| [schwab-mcp](https://github.com/lonniev/schwab-mcp) | Production operator — brokerage data with OAuth2 |
| [cypher-mcp](https://github.com/lonniev/cypher-mcp) | Production operator — monetized graph answers via named Cypher templates over Neo4j/AuraDB |
| [taxsort-mcp](https://github.com/lonniev/taxsort-mcp) | Production operator — tax sorting + classification with Cloudflare Pages UI |
| [optionality-mcp](https://github.com/lonniev/optionality-mcp) | Production operator — options analytics |
| [tollbooth-shortlinks](https://github.com/lonniev/tollbooth-shortlinks) | Community utility — URL shortener |
| [tollbooth-oauth2-collector](https://github.com/lonniev/tollbooth-oauth2-collector) | Community utility — OAuth2 callback mailbox |
| [tollbooth-pricing-studio](https://github.com/lonniev/tollbooth-pricing-studio) | iOS pricing editor — Nostr DMs, native crypto |

---

## Install

```bash
# 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.

```bash
# 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 — it is drawn from the shared `relays.json` registry in the dpyc-community repo (single source of truth, cached with a 3-day TTL).

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

```bash
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](https://stablecoin.myshopify.com/blogs/our-value/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](https://github.com/lonniev/dpyc-community/blob/main/TRADEMARKS.md) in the dpyc-community repository for usage guidelines.

## License

Apache 2.0 — see [LICENSE](LICENSE).

---

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