If you followed Parts 3 and 4, you deposited testnet sats and spent some reading a paywalled article. Where did the money go?
curl -H "Authorization: Nostr $NIP98_TOKEN" \ http://localhost:4443/pay/.balance
{
"did": "did:nostr:5e7617...45af",
"balance": 40,
"unit": "sat"
}
Forty sats. You deposited 50, spent 10 reading an article. Your balance is on your server. Not Stripe's server. Not PayPal's server. A file on your machine.
The balance is stored in a webledger — a JSON file at a well-known path on your pod:
cat data/.well-known/webledgers/webledgers.json
{
"entries": [
{
"url": "did:nostr:5e7617...45af",
"amount": "40"
}
]
}
That's it. A JSON file with entries. Each entry has an identity (url) and a balance (amount). No database. No encrypted blob. Just a file you can read, back up, rsync, or version with git.
Traditional payment systems store your balance in their database, behind their API, under their terms of service. A webledger stores it in your filesystem, under your control, in a format anyone can read. The webledgers.org spec defines the format so different implementations can interoperate.
In Part 3, you deposited testnet sats by posting a TXO URI. Let's unpack what that means.
A TXO (Transaction Output) is a pointer to sats on the Bitcoin blockchain. Every Bitcoin transaction creates outputs — specific amounts at specific locations. A TXO URI identifies one:
txo:tbtc4:a1b2c3d4e5f6...789:0
| Part | Meaning |
|---|---|
txo: | It's a TXO URI |
tbtc4: | Chain — testnet4 (free sats for development) |
a1b2c3...789 | Transaction ID (the hash of the transaction) |
:0 | Output index (first output in this transaction) |
When you POST this to /pay/.deposit, the server:
The server verifies that a transaction output exists on the blockchain and credits the equivalent amount in the webledger. You can also send sats directly to your personal deposit address (GET /pay/.address?user=did:nostr:YOUR_KEY) — the pod detects the deposit automatically when you check your balance.
Remember the Nostr key from Part 2? The same secp256k1 key that signs your NIP-98 auth events is also a valid Bitcoin taproot key. So the full flow uses one key for everything:
| Step | What Happens | Key Used |
|---|---|---|
| Get sats | Faucet sends to your taproot address | Your pubkey |
| Deposit | POST TXO URI + NIP-98 signature | Your privkey signs |
| Verify | Server checks chain, credits did:nostr | Your pubkey = identity |
| Access | NIP-98 signature + balance check | Your privkey signs |
| Spend | Server deducts from webledger | Your did:nostr = account |
One keypair. Authentication, payment address, identity, and account — all the same 32 bytes.
Here's what's not involved:
| Traditional | HTTP 402 + Webledger |
|---|---|
| Bank holds your money | Balance is a file on your pod |
| Payment processor takes a cut | No intermediary |
| KYC / identity verification | Nostr key (pseudonymous) |
| Chargebacks, disputes, freezes | Sats verified on chain are final |
| API keys, webhooks, SDKs | One curl command |
| Terms of service | Your server, your rules |
The webledger is a local accounting system. It records what was proven (via TXO) and what was spent (via PaymentCondition). The source of truth is the blockchain for deposits and the file for balances. Both are verifiable. Neither requires trust.
Part 6: Anchored to Bitcoin. Your balance is a JSON file. How do you prove it's real? Blocktrails anchor state to Bitcoin using key chaining — the same secp256k1 math your Nostr key uses. No sidechain, no trust, just cryptography.