L402 micropayment middleware for discourse. Trust-weighted, progressive pricing for forums and APIs.
Python port of discourse-toll (Node.js).
The thesis: Karma selects for popularity. Rate limiting is indiscriminate. Economic cost selects for intentionality.
pip install discourse-tollWith FastAPI support:
pip install discourse-toll[fastapi]from fastapi import FastAPI, Depends
from discourse_toll import create_toll, TollConfig, PricingConfig, MockWallet
app = FastAPI()
mock = MockWallet() # Replace with real wallet in production
toll = create_toll(TollConfig(
secret="your-hmac-secret",
wallet=mock,
pricing=PricingConfig(
base_sats=1,
progressive_multiplier=1.5,
progressive_cap=50,
),
))
@app.post("/api/comments")
async def post_comment(
request: Request,
paid: bool = Depends(toll(context_from="body.thread_id")),
):
return {"ok": True}from discourse_toll import create_discourse_client
client = create_discourse_client(
pay_fn=my_wallet.pay_invoice, # async (invoice) -> {"preimage": str}
max_sats=50,
agent_id="my-nostr-pubkey",
)
result = await client.post(
"https://forum.example/api/comments",
json={"text": "Worth paying for", "thread_id": "abc"},
)
print(result.paid, result.sats) # True, 1| Scenario | Cost |
|---|---|
| First comment in a thread | 1 sat |
| Second comment, same thread | 2 sats |
| Third comment, same thread | 3 sats |
| 10th comment, same thread | 38 sats |
| First comment in a different thread | 1 sat |
| Any comment with trust score ≥ 80 | Free |
| Any comment with trust score 30-79 | 50% off |
| Comment after waiting > 1 minute | 25% off |
from discourse_toll import static_resolver, api_resolver
# Static (testing)
toll = create_toll(TollConfig(
secret="test",
wallet=mock,
trust=static_resolver({"pubkey-1": 90, "pubkey-2": 40}),
))
# ai.wot REST API (production)
toll = create_toll(TollConfig(
secret="test",
wallet=mock,
trust=api_resolver("https://wot.jeletor.cc"),
))Implement the wallet protocol:
class MyWallet:
async def create_invoice(self, sats: int, description: str) -> dict:
"""Return {"invoice": "lnbc...", "payment_hash": "hex"}"""
...
async def lookup_invoice(self, payment_hash: str) -> dict:
"""Return {"paid": bool, "preimage": "hex" | None}"""
...Or use MockWallet for testing (no Lightning needed).
- Only give the server wallet
make_invoiceandlookup_invoicepermissions - Macaroon caveats are locked to: expiry, endpoint, method, context, agent
- Fail open on errors (availability > enforcement)
- Preimage verification: SHA256(preimage) must equal payment_hash
Python port of the Node.js ecosystem:
- discourse-toll — Original Node.js package
- ai-wot — Decentralized trust scores
- lightning-toll — L402 protocol
MIT