Skip to content

Commit 0b51512

Browse files
committed
examples: add runnable BRC-105 server + client demo
1 parent 66f2041 commit 0b51512

3 files changed

Lines changed: 260 additions & 0 deletions

File tree

examples/README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Examples
2+
3+
Runnable examples for `bsv-brc`. Each one is self-contained and small enough
4+
to read in a single sitting.
5+
6+
## ⚠️ Demo only — read this first
7+
8+
**These examples use fake stubs** for the parts that require a real BSV
9+
wallet. They demonstrate the *protocol flow*, not real cryptographic
10+
verification. Specifically:
11+
12+
- `verify_payment` always accepts — production code must call your wallet's
13+
`internalizeAction()` (or equivalent) and return the real result.
14+
- `build_payment` returns a hardcoded fake transaction — production code
15+
must call your wallet's `createAction()` to build a real signed payment.
16+
- `get_identity_key` bypasses BRC-103 authentication for local testing —
17+
production code must use real BRC-103/104 mutual auth (coming in a future
18+
release of `bsv-brc`).
19+
20+
**Do not deploy these examples as-is.** They will accept any payment (or
21+
none at all) and they will not protect you from anything. They exist so
22+
you can see the shape of the API in 30 seconds without needing a wallet
23+
running on your machine.
24+
25+
## brc105 — HTTP 402 micropayments
26+
27+
The `BRC-105` examples show the server and client sides of the 402 Payment
28+
Required flow. The two scripts talk to each other over HTTP on
29+
`127.0.0.1:8000`.
30+
31+
### Run
32+
33+
In one terminal:
34+
35+
```bash
36+
pip install "bsv-brc[starlette]" uvicorn httpx
37+
python examples/brc105_minimal_server.py
38+
```
39+
40+
In another terminal:
41+
42+
```bash
43+
python examples/brc105_client.py
44+
```
45+
46+
### What you should see
47+
48+
Server side:
49+
```
50+
INFO: Uvicorn running on http://127.0.0.1:8000
51+
INFO: 127.0.0.1:xxxxx - "GET /data HTTP/1.1" 402 Payment Required
52+
INFO: 127.0.0.1:xxxxx - "GET /data HTTP/1.1" 200 OK
53+
```
54+
55+
Client side:
56+
```
57+
→ GET /data (no payment)
58+
← 402
59+
challenge: 100 satoshis required
60+
→ GET /data (with payment)
61+
← 200
62+
body: {'data': 'hello, paying customer', 'satoshis_paid': 100}
63+
satoshis_paid header: 100
64+
```
65+
66+
### What just happened
67+
68+
1. Client requested a paid endpoint without including a payment header.
69+
2. Server's `PaymentMiddleware` saw no payment, generated a derivation
70+
prefix nonce, and returned `402 Payment Required` with the challenge
71+
in the response headers.
72+
3. Client parsed the challenge, built a payment (fake, in this example),
73+
and retried the request with the `x-bsv-payment` header.
74+
4. Server's middleware verified the nonce, called `verify_payment` (which
75+
in this demo always accepts), and let the request through to the
76+
handler.
77+
5. Handler returned the response, and the middleware added an
78+
`x-bsv-payment-satoshis-paid` header to confirm the amount.
79+
80+
In a real deployment, steps 2 and 3 would involve actual BSV wallet
81+
operations and the payment would be a signed transaction the server can
82+
broadcast and verify on-chain.

examples/brc105_client.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
BRC-105 minimal client example.
3+
4+
Walks through the full 402 Payment Required flow against the example server:
5+
6+
1. Hit /data without payment → 402 with payment challenge
7+
2. Build a BSVPayment from the challenge → POST again with x-bsv-payment
8+
3. Server accepts → 200 with the response
9+
10+
Run the server first:
11+
python examples/brc105_minimal_server.py
12+
13+
Then in another terminal:
14+
python examples/brc105_client.py
15+
16+
⚠️ DEMO ONLY — DO NOT DEPLOY ⚠️
17+
The `build_payment` function in this example produces a fake payment with
18+
hardcoded values. In production you must call your BSV wallet's
19+
`createAction()` to build a real signed transaction that pays the server's
20+
derived output script.
21+
"""
22+
23+
from __future__ import annotations
24+
25+
import asyncio
26+
import json
27+
28+
import httpx
29+
30+
from bsv_brc.brc105 import BSVPayment, PaymentChallenge, parse_challenge_headers
31+
32+
33+
SERVER_URL = "http://127.0.0.1:8000"
34+
35+
36+
async def fake_build_payment(challenge: PaymentChallenge) -> BSVPayment:
37+
"""
38+
DEMO ONLY. Returns a hardcoded fake payment.
39+
In production, call wallet.createAction() with the challenge to build
40+
a real signed BSV transaction paying the requested amount.
41+
"""
42+
return BSVPayment(
43+
derivation_prefix=challenge.derivation_prefix,
44+
derivation_suffix="demo-suffix",
45+
transaction="BASE64_FAKE_PAYMENT_TX_NOT_REAL", # not a real AtomicBEEF
46+
)
47+
48+
49+
async def main() -> None:
50+
async with httpx.AsyncClient() as client:
51+
# Step 1: hit /data with no payment
52+
print("→ GET /data (no payment)")
53+
r1 = await client.get(
54+
f"{SERVER_URL}/data",
55+
headers={"x-bsv-auth-identity-key": "demo-client-identity-key"},
56+
)
57+
print(f" ← {r1.status_code}")
58+
59+
if r1.status_code != 402:
60+
print(f" unexpected response: {r1.text}")
61+
return
62+
63+
# Step 2: parse the challenge from headers and build a fake payment
64+
challenge = parse_challenge_headers(dict(r1.headers))
65+
if challenge is None:
66+
print(" challenge headers missing or malformed")
67+
return
68+
print(f" challenge: {challenge.satoshis_required} satoshis required")
69+
70+
payment = await fake_build_payment(challenge)
71+
72+
# Step 3: retry with the payment header
73+
print("→ GET /data (with payment)")
74+
r2 = await client.get(
75+
f"{SERVER_URL}/data",
76+
headers={
77+
"x-bsv-auth-identity-key": "demo-client-identity-key",
78+
"x-bsv-payment": json.dumps(payment.to_dict()),
79+
},
80+
)
81+
print(f" ← {r2.status_code}")
82+
print(f" body: {r2.json()}")
83+
print(f" satoshis_paid header: {r2.headers.get('x-bsv-payment-satoshis-paid')}")
84+
85+
86+
if __name__ == "__main__":
87+
asyncio.run(main())

examples/brc105_minimal_server.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""
2+
BRC-105 minimal server example.
3+
4+
A Starlette app with one paid endpoint (`/data`) that costs 100 satoshis,
5+
and one free endpoint (`/health`).
6+
7+
Run:
8+
pip install "bsv-brc[starlette]" uvicorn
9+
python examples/brc105_minimal_server.py
10+
11+
Then in another terminal:
12+
python examples/brc105_client.py
13+
14+
⚠️ DEMO ONLY — DO NOT DEPLOY ⚠️
15+
This example uses a fake `verify_payment` stub that accepts every payment
16+
without actually verifying anything on-chain. In production you must wire
17+
this up to a real BSV wallet via `wallet.internalizeAction()` (or equivalent).
18+
The `get_identity_key` override here also bypasses BRC-103 authentication
19+
for local testing — production code must NOT do this.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
from starlette.applications import Starlette
25+
from starlette.requests import Request
26+
from starlette.responses import JSONResponse
27+
from starlette.routing import Route
28+
29+
from bsv_brc.brc105 import (
30+
BSVPayment,
31+
NonceManager,
32+
PaymentMiddleware,
33+
PaymentResult,
34+
StaticPricing,
35+
)
36+
37+
38+
# --- handlers ---
39+
40+
async def health(_: Request) -> JSONResponse:
41+
return JSONResponse({"status": "ok"})
42+
43+
44+
async def data(request: Request) -> JSONResponse:
45+
# PaymentMiddleware attaches the result to request.state on success.
46+
paid = request.state.payment.satoshis_paid
47+
return JSONResponse({"data": "hello, paying customer", "satoshis_paid": paid})
48+
49+
50+
# --- payment plumbing (DEMO STUBS — replace in production) ---
51+
52+
async def fake_verify_payment(payment: BSVPayment, identity_key: str) -> PaymentResult:
53+
"""
54+
DEMO ONLY. Always accepts. In production, call your BSV wallet's
55+
internalizeAction() with the payment's transaction and return the
56+
real result from the wallet.
57+
"""
58+
return PaymentResult(satoshis_paid=100, accepted=True)
59+
60+
61+
def fake_get_identity_key(_: Request) -> str:
62+
"""
63+
DEMO ONLY. Bypasses BRC-103 auth for local testing.
64+
Production code must read a real identity key set by BRC-104 middleware.
65+
"""
66+
return "demo-identity-key-not-for-production"
67+
68+
69+
# --- app ---
70+
71+
app = Starlette(
72+
routes=[
73+
Route("/health", health),
74+
Route("/data", data),
75+
],
76+
)
77+
78+
app.add_middleware(
79+
PaymentMiddleware,
80+
nonce_manager=NonceManager(secret=b"demo-server-secret-change-me"),
81+
pricing=StaticPricing(100), # 100 satoshis per request
82+
verify_payment=fake_verify_payment,
83+
get_identity_key=fake_get_identity_key,
84+
excluded_paths={"/health"},
85+
)
86+
87+
88+
if __name__ == "__main__":
89+
import uvicorn
90+
91+
uvicorn.run(app, host="127.0.0.1", port=8000)

0 commit comments

Comments
 (0)