{
  "markdown": "# tollbooth-sample\n\nEducational Weather Stats MCP Service — the reference implementation for building\nTollbooth DPYC monetized API services with Bitcoin Lightning micropayments.\n\nThis service wraps the free [Open-Meteo](https://open-meteo.com) weather API\nand gates paid tool calls through the [Tollbooth](https://github.com/lonniev/tollbooth-dpyc)\ncredit system using the `@runtime.paid_tool()` decorator. Domain tools contain\nonly business logic; debit, rollback, balance warnings, and constraint evaluation\nare handled automatically by the `OperatorRuntime`. Standard DPYC tools\n(balance, purchase, Secure Courier, Oracle, pricing, constraints) are delegated\nto the wheel via `register_standard_tools()`.\n\n**Version:** 0.4.2\n\n## Build your own operator — the `bootstrap-dpyc-operator` skill\n\nThis repo doubles as a **Claude Code plugin**. The `bootstrap-dpyc-operator` skill turns your\n**existing REST API, stdio MCP, or HTTP MCP** into a monetized DPYC Operator MCP: it clones this\ntemplate live, wraps your domain logic, and generates a deploy-ready project. You keep writing\nbusiness logic — the SDK handles payments, identity, vault, audit, and pricing.\n\nInstall it in Claude Code:\n\n```\n/plugin marketplace add lonniev/tollbooth-sample\n/plugin install bootstrap-dpyc-operator@tollbooth-dpyc\n```\n\nThen ask Claude to *\"make my API a paid DPYC operator\"* — the skill activates automatically by\nits description. It never touches your original code (it emits a sibling `<slug>-mcp/` project)\nand reads this repo's live wheel pin on every run, so it can't go stale.\n\nSee [`skills/bootstrap-dpyc-operator/`](skills/bootstrap-dpyc-operator/) for the skill and its\nreference guides (canonical pattern, source adapters, sessions & vaults, onboarding checklist).\n\n## The DPYC Economy\n\n**DPYC** stands for **Don't Pester Your Customer**. It's a philosophy and\nprotocol for API monetization that eliminates mid-session payment popups,\nsubscription nag screens, and KYC friction.\n\n### How it works\n\n1. **Pre-funded balances** — Users buy credits via Bitcoin Lightning *before*\n   using tools. Each tool call silently debits from their balance. No\n   interruptions, no \"please upgrade\" modals.\n\n2. **Nostr keypair identity** — Users are identified by a Nostr public key\n   (`npub`), not an email or password. One keypair per role, managed by the\n   user. No account creation forms.\n\n3. **UUID-keyed tool identity** — Every tool is a `ToolIdentity` object with\n   a deterministic UUID v5 derived from a capability name. Pricing hints come\n   from the `category` field:\n\n   | Category | Pricing hint | Use case                    |\n   |----------|--------------|-----------------------------|\n   | `free`   | 0 sats       | Balance checks, status      |\n   | `read`   | 1 sat        | Simple lookups              |\n   | `write`  | 5 sats       | Multi-step operations       |\n   | `heavy`  | 10 sats      | Expensive queries           |\n\n   Actual prices are set dynamically by the operator's pricing model in Neon.\n\n4. **Rollback on failure** — If the downstream API fails after a debit,\n   credits are automatically rolled back via a compensating tranche. The\n   user never pays for a failed call.\n\n5. **Social Contract** — The DPYC ecosystem is a voluntary community bound\n   by transparent, auditable economic rules, with a Certification Chain that\n   cascades trust from the root:\n   - **Citizens** — Users who consume API services\n   - **Operators** — Developers who run MCP services (like this one)\n   - **Authorities** — Certify operators and collect a small tax on purchases\n   - **First Curator** — The root of the chain, mints the initial cert-sat supply\n\n## How Tollbooth Monetization Works\n\n### ToolIdentity and the frozen `tool_id`\n\nEach domain tool is registered as a `ToolIdentity` with a **frozen `tool_id`**\n(an opaque UUID), a capability name, a category (pricing hint), and an intent\ndescription. Mint the UUID **once** at the tool's birth — run\n`capability_uuid(\"get_current_weather\")` at a REPL (or `uuid.uuid4()`), then\npaste the result as a literal constant and never change it again. Freezing the\nliteral is what lets you rename a capability later without orphaning its pricing\nrows in Neon. Do **not** call `capability_uuid(...)` at runtime; the identity\nmust live in exactly one place:\n\n```python\nfrom tollbooth.tool_identity import ToolIdentity, STANDARD_IDENTITIES\nfrom tollbooth.runtime import OperatorRuntime, register_standard_tools\nfrom tollbooth.credential_templates import CredentialTemplate, FieldSpec\nfrom tollbooth.credential_validators import validate_btcpay_creds\n\n# Frozen UUIDs — minted once at tool birth, never recomputed.\nGET_CURRENT_WEATHER_UUID    = \"b7327eb8-92b4-5252-84e0-ba3f437a16ed\"\nGET_WEATHER_FORECAST_UUID   = \"b6d0e596-3aec-5a62-980b-7875aa04d079\"\nGET_HISTORICAL_WEATHER_UUID = \"5608f3e9-44c4-5b28-9744-704af6d701f0\"\n\n# 1. Define domain tool identities\n_DOMAIN_TOOLS = [\n    ToolIdentity(\n        tool_id=GET_CURRENT_WEATHER_UUID,\n        capability=\"get_current_weather\",\n        category=\"read\",\n        intent=\"Get current weather conditions\",\n    ),\n    ToolIdentity(\n        tool_id=GET_WEATHER_FORECAST_UUID,\n        capability=\"get_weather_forecast\",\n        category=\"write\",\n        intent=\"Get weather forecast\",\n    ),\n    ToolIdentity(\n        tool_id=GET_HISTORICAL_WEATHER_UUID,\n        capability=\"get_historical_weather\",\n        category=\"heavy\",\n        intent=\"Get historical weather data\",\n    ),\n]\n\nTOOL_REGISTRY: dict[str, ToolIdentity] = {ti.tool_id: ti for ti in _DOMAIN_TOOLS}\n```\n\n### The `@runtime.paid_tool()` decorator\n\nEvery paid tool is a single decorator away from full DPYC monetization.\nThe decorator takes the tool's frozen `tool_id` constant and handles debit,\nbalance checks, constraint evaluation, rollback on failure, and low-balance\nwarnings automatically. Your tool function contains only domain logic:\n\n```python\nfrom typing import Annotated, Any\nfrom pydantic import Field\nfrom fastmcp import FastMCP\n\nmcp = FastMCP(\"tollbooth-sample\", ...)\n\n# Create the runtime with merged standard + domain identities\nruntime = OperatorRuntime(\n    tool_registry={**STANDARD_IDENTITIES, **TOOL_REGISTRY},\n    operator_credential_template=CredentialTemplate(\n        service=\"tollbooth-sample-operator\",\n        version=2,\n        description=\"Operator credentials for BTCPay Lightning payments\",\n        fields={\n            \"btcpay_host\": FieldSpec(required=True, sensitive=True, ...),\n            \"btcpay_api_key\": FieldSpec(required=True, sensitive=True, ...),\n            \"btcpay_store_id\": FieldSpec(required=True, sensitive=True, ...),\n        },\n    ),\n    credential_validator=validate_btcpay_creds,\n    ...\n)\n\n# Delegate all standard DPYC tools to the wheel.\n# register_standard_tools returns the slug-prefixed @tool decorator —\n# use it for the operator's own paid tools below.\ntool = register_standard_tools(mcp, \"weather\", runtime, ...)\n\n# Decorate each paid domain tool\n@tool\n@runtime.paid_tool(GET_CURRENT_WEATHER_UUID)\nasync def current(\n    latitude: float,\n    longitude: float,\n    npub: Annotated[str, Field(\n        description=\"Required. Your Nostr public key (npub1...) for credit billing.\"\n    )] = \"\",\n    dpop_token: str = \"\",\n) -> dict[str, Any]:\n    \"\"\"Get current weather conditions for a location.\n\n    Returns temperature, wind speed, and weather code from Open-Meteo.\n    \"\"\"\n    return await weather.get_current(latitude, longitude)\n```\n\nThat is the complete paid tool. No manual debit calls, no try/except\nrollback blocks, no balance-warning plumbing. The decorator:\n\n- Looks up the tool's pricing from the `ToolIdentity` registry by UUID\n- Extracts `npub` from the function arguments for billing\n- Validates `dpop_token` for operator proof verification\n- Debits before calling your function (respecting ConstraintGate discounts)\n- Rolls back automatically if your function raises an exception\n- Appends a low-balance warning to the response when funds are running low\n- Skips all gating in STDIO mode so local development works without credits\n\n### Key patterns\n\n**`register_standard_tools(mcp, \"weather\", runtime, …)`** — Registers all\nstandard DPYC tools (balance, purchase, payment, pricing, Secure Courier,\nOracle, constraints) from the tollbooth-dpyc wheel, mounts oracle\ndelegations under `<slug>_oracle_*`, and **returns** the slug-prefixed\n`@tool` decorator. Capture the return so you can use the same decorator\nfor your own paid tools — every wire-exposed name on this operator then\nshares one slug prefix.\n\n**`validate_btcpay_creds`** — Credential validator that checks BTCPay\ncredentials at receive time, not at first use. Invalid credentials are\nrejected immediately during the Secure Courier exchange.\n\n**`CredentialTemplate`** — Declares the operator's required secrets\n(BTCPay host, API key, store ID) so the Secure Courier flow can prompt\nfor the right fields and validate them on delivery.\n\n### The `npub` and `dpop_token` parameters\n\nEvery paid tool must accept `npub` and `dpop_token` keyword arguments. The\n`npub` tells the runtime which patron to bill; `dpop_token` carries the\noperator proof for verification:\n\n```python\nnpub: Annotated[str, Field(\n    description=\"Required. Your Nostr public key (npub1...) for credit billing.\"\n)] = \"\"\ndpop_token: str = \"\"\n```\n\nThe defaults of `\"\"` keep both parameters optional in STDIO/dev mode.\n\n### What the runtime handles under the hood\n\n```\nTool call arrives\n    |\n    v\n@runtime.paid_tool(GET_CURRENT_WEATHER_UUID)\n    |\n    +-- UUID lookup in tool_registry -> ToolIdentity + pricing\n    +-- npub + dpop_token extraction from kwargs\n    +-- STDIO mode? --yes--> Skip gating, call function directly\n    |\n    +-- ConstraintGate evaluation (discounts, surge, supply caps)\n    +-- Balance check + debit\n    |       |\n    |       insufficient --> Return error (no function call)\n    |\n    +-- Call your function\n    |       |\n    |       exception --> Automatic rollback, return error\n    |\n    +-- Append low-balance warning if needed\n    |\n    v\nReturn result to caller\n```\n\n## Constraint Engine\n\nThe ConstraintGate is an opt-in dynamic pricing layer. Enable it by setting:\n\n```bash\nCONSTRAINTS_ENABLED=true\nCONSTRAINTS_CONFIG='{\"tool_constraints\": {...}}'\n```\n\nCommon constraint types (the SDK registry holds more — `weather_list_constraint_types`\nenumerates the full set live):\n\n| Type              | Effect                                            |\n|-------------------|---------------------------------------------------|\n| `free_trial`      | First N calls are free                            |\n| `happy_hour`      | Discount during specific hours                    |\n| `temporal_window` | Allow calls only during a time window             |\n| `finite_supply`   | Cap total invocations globally                    |\n| `loyalty_discount`| Discount after spending N sats                    |\n| `bulk_bonus`      | Discount after N invocations                      |\n| `surge_pricing`   | Demand-elastic multiplier during high demand      |\n\nUse `weather_check_price` to preview constraint effects without spending credits.\n\nSee [`constraints/example_basic.json`](constraints/example_basic.json),\n[`constraints/example_advanced.json`](constraints/example_advanced.json), and\n[`constraints/example_surge.json`](constraints/example_surge.json)\nfor configuration examples.\n\n## Becoming an Operator\n\nNew to Tollbooth? See **[GETTING-STARTED.md](GETTING-STARTED.md)** for a\nstep-by-step guide covering Nostr keypair setup, Authority enrollment,\nBTCPay configuration, and deploying your first monetized MCP service.\n\n## Quick Start\n\n### Local development (no gating)\n\n```bash\ngit clone https://github.com/lonniev/tollbooth-sample.git\ncd tollbooth-sample\npip install -e \".[dev]\"\npython -m tollbooth_sample.server\n```\n\nIn STDIO mode, all tools work without credits — great for development.\n\n### Deploy on Prefect Horizon\n\nThe hosting platform is **Prefect Horizon** (FastMCP is the runtime/framework\nthe server is built on).\n\n1. Push to GitHub\n2. Connect the repo on Prefect Horizon\n3. Set environment variables:\n   - `TOLLBOOTH_NOSTR_OPERATOR_NSEC` — Nostr key for identity bootstrap\n     (the only env var required to boot; all other secrets are delivered\n     via Secure Courier credential templates)\n   - (Optional) `CONSTRAINTS_ENABLED=true` + `CONSTRAINTS_CONFIG=...`\n\n> **Heads-up for operators with long-running tools.** By default, claim-check /\n> async jobs run in-memory (`async_jobs.backend: \"memory\"`), which means **they do\n> not survive a Horizon recycle** (`durable_across_recycles: false`). That's fine for\n> this reference sample, which has no long-runners — but if you add a tool that defers\n> work to a background job, pin the `[prefect]` extra and deliver the durable-executor\n> secrets (`prefect_api_url` / `prefect_api_key` / `closure_seal_key`, the\n> `LONGRUNNER_CREDENTIAL_FIELDS`) via Secure Courier so jobs settle across redeploys.\n> Check your live state anytime with `service_status.async_jobs`.\n\n### Run tests\n\n```bash\npip install -e \".[dev]\"\npytest -v\n```\n\n## Tool Reference\n\n| MCP tool name              | Cost     | Description                           |\n|----------------------------|----------|---------------------------------------|\n| `weather_current`          | read     | Current weather for lat/lon           |\n| `weather_forecast`         | write    | Multi-day forecast (1-16 days)        |\n| `weather_historical`       | heavy    | Historical weather for a date range   |\n| `weather_check_balance`    | free     | Check credit balance                  |\n| `weather_purchase_credits` | free     | Buy credits via Lightning             |\n| `weather_check_payment`    | free     | Check invoice status                  |\n| `weather_request_adoption` | free     | Request adoption by an Authority (deferred-courtship onboarding) |\n| `weather_check_price`      | free     | Preview cost (shows constraint effects)|\n| `weather_service_status`   | free     | Health + constraint config summary    |\n| `weather_oracle_how_to_join`      | free | DPYC onboarding instructions          |\n| `weather_oracle_get_tax_rate`     | free | Current certification tax rate        |\n| `weather_oracle_lookup_member`    | free | Look up a DPYC member                 |\n| `weather_oracle_about`            | free | DPYC ecosystem description            |\n| `weather_oracle_network_advisory` | free | Active network advisories             |\n\n## DPYC Ecosystem\n\n**Core**\n\n- [tollbooth-dpyc](https://github.com/lonniev/tollbooth-dpyc) — Python SDK (vault, auth, pricing, Lightning, Nostr identity)\n- [dpyc-community](https://github.com/lonniev/dpyc-community) — Governance registry: membership, advisories, threat model\n- [dpyc-oracle](https://github.com/lonniev/dpyc-oracle) — Community concierge (free onboarding + member lookup)\n- [tollbooth-authority](https://github.com/lonniev/tollbooth-authority) — Certification backbone (Schnorr-signed certificates)\n- [tollbooth-sample](https://github.com/lonniev/tollbooth-sample) — Sample Operator (this canonical template)\n- [tollbooth-pricing-studio](https://github.com/lonniev/tollbooth-pricing-studio) — iOS pricing-model editor / operator console\n\n**Operators**\n\n- [cypher-mcp](https://github.com/lonniev/cypher-mcp) — Monetized graph answers: named Cypher templates over Neo4j/AuraDB\n- [schwab-mcp](https://github.com/lonniev/schwab-mcp) — Charles Schwab brokerage data\n- [thebrain-mcp](https://github.com/lonniev/thebrain-mcp) — TheBrain personal knowledge graph\n- [excalibur-mcp](https://github.com/lonniev/excalibur-mcp) — X/Twitter posting\n- [taxsort-mcp](https://github.com/lonniev/taxsort-mcp) — Tax classification + Cloudflare Pages UI\n- [optionality-mcp](https://github.com/lonniev/optionality-mcp) — Options analytics (brokerage-data operator)\n\n**Advocates & utilities**\n\n- [tollbooth-oauth2-collector](https://github.com/lonniev/tollbooth-oauth2-collector) — OAuth2 callback handler (advocate service)\n- [tollbooth-shortlinks](https://github.com/lonniev/tollbooth-shortlinks) — URL shortener utility\n\n## License\n\nApache-2.0\n",
  "bytes": 16076,
  "sha": "3f2d12c49f30b2a365c2916a2622495f1c7bd45a7fe5a3e3603a75cad131dc3d",
  "repo_slug": "lonniev/tollbooth-sample",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_lonniev_tollbooth_sample_c15ea002/readme"
}