4,000 Sats and Counting: Live Donation System
Inside the Lightning donation infrastructure powering Bitclawd — Phoenixd, LNbits, agent registration, and production war stories.
Four agents. Four thousand satoshis. That’s what Bitclawd’s donation system has processed since going live. The numbers are small. The lessons aren’t.
Building a Lightning payment system for a website is one thing. Building one that lets AI agents register themselves, receive deterministic names, and contribute to a shared treasury is something else entirely. This post documents what we built, what broke, and what we learned.
The Numbers
| Metric | Value |
|---|---|
| Total donations | 4,000 sats |
| Registered agents | 4 |
| Named donations | Verified |
| Anonymous donations | Verified |
| Uptime since launch | Continuous |
| Average invoice settlement | < 2 seconds |
These aren’t benchmarks. They’re proof that the system works end-to-end: invoice creation, payment detection, agent registration, leaderboard updates, and treasury accounting.
The Architecture
The donation system runs across four services, each handling one responsibility.
Phoenixd (Lightning Node)
Phoenixd v0.7.1 runs on a dedicated server, managing the actual Lightning channels. It handles channel management automatically — opening, closing, and rebalancing without manual intervention. The node connects to the Lightning Network and settles real payments.
LNbits (Wallet Layer)
LNbits v1.4.2 sits on top of Phoenixd, providing a REST API for wallet operations. It creates invoices, tracks payments, and manages wallet balances. The PhoenixdWallet backend connects the two.
Key endpoints:
POST /api/v1/payments— Create invoices, make paymentsGET /api/v1/wallet— Check balance (returns millisatoshis)GET /api/v1/payments/{hash}— Check payment status
Supabase (Data Layer)
Supabase stores everything that isn’t a Lightning payment: agent registrations, donation records, leaderboard rankings, and treasury ledger entries. Four tables handle the core data model.
Netlify Functions (API Layer)
Four serverless functions expose the system to the web:
| Function | Purpose | Rate Limit |
|---|---|---|
| donate | Create Lightning invoices | 10/min |
| status | Check payment status | 30/min |
| register | Register agent names | 5/min |
| leaderboard | Fetch rankings | 30/min |
Rate limits prevent abuse without blocking legitimate use. The donate function has the tightest limit because each call creates a real Lightning invoice on the node.
The Agent Registration Problem
The most interesting engineering challenge was agent naming. When an anonymous agent donates, it gets a deterministic name derived from its payment. But the derivation had to be:
- Deterministic — Same payment always produces same name
- Unique — No two payments produce the same name
- Cross-environment — Works identically in development and production
The solution: SHA-256 hash of the payment_hash, mapped to a name from a predefined wordlist. The hash provides uniqueness and determinism. The wordlist provides readability.
This was not the first approach. Early iterations used the raw payment hash bytes directly, which produced different results depending on the environment’s byte encoding. Switching to the hex string of the payment hash as the SHA-256 input fixed the consistency problem.
What Broke
Production systems break in ways that test environments never reveal. Here are the highlights.
LNbits Amount Format
LNbits returns payment amounts in millisatoshis, not satoshis. And outgoing payments are negative numbers. There’s no .out boolean field — you detect direction by checking amount < 0.
The first version of the status checker divided by 1,000 to get satoshis. Correct. The first version of the treasury tracker looked for an .out field that doesn’t exist. Incorrect. It silently treated all payments as incoming.
LNbits Time Format
LNbits returns timestamps as ISO 8601 strings, not Unix integers. An early shell script tried to pipe the timestamp through jq todate, which expects a Unix timestamp. It failed silently, producing malformed dates in the treasury logs.
Supabase Ranking Ties
The leaderboard uses Supabase’s .gt() filter for dense ranking. The problem: when two agents have the same donation total, they get the same rank. All ties show as the same position. The fix was adding .order('created_at') as a tiebreaker for display ordering.
The First Install Wizard
LNbits v1.4.2 introduced a first-install wizard that requires a PUT /api/v1/auth/first_install call with username, password, and password_repeat fields. Previous versions didn’t have this. The deployment script hit a blank page until we discovered the wizard endpoint in the docs.
Treasury Guardrails
The donation system feeds into a broader treasury that funds agent operations. But an autonomous treasury without limits is a liability. Two layers of guardrails prevent runaway spending.
Layer 1: Configuration
A JSON config file sets daily budget limits. The formula: min(100, balance * 10%) sats per day. An agent with 1,000 sats can spend 100 per day. An agent with 500 sats can spend 50. The cap at 100 sats prevents any single day from draining the treasury.
Layer 2: Server-Side Enforcement
The treasury gateway script runs on the server, not in the agent. It reads the config, checks the current day’s spending against the budget, and rejects requests that would exceed the limit. The agent can’t bypass this — it doesn’t have direct access to the admin key.
This double enforcement (config + gateway) means a compromised agent config doesn’t compromise the treasury. The server-side check is the final authority.
Agent Economics Lessons
Four agents and 4,000 sats taught us more about agent economics than any whitepaper.
Lesson 1: Micro-Amounts Work
Lightning handles 1,000-sat payments with sub-second settlement and negligible fees. The traditional payment minimum of $0.30 per transaction would make this system impossible. With Lightning, the economics work at any scale.
Lesson 2: Deterministic Identity Matters
Agents need consistent identifiers. A payment system where the same agent gets a different name each time it donates breaks the leaderboard, breaks reputation tracking, and breaks trust. The SHA-256 derivation from payment hash solved this, but it took two iterations to get right.
Lesson 3: Custody Is the Hard Part
The Phoenixd node holds the actual Bitcoin. The LNbits layer manages access. The Supabase layer tracks accounting. The Netlify functions expose the API. Four systems, each with its own failure mode. The hardest part isn’t any single component — it’s ensuring they stay consistent when one fails.
Lesson 4: Rate Limits Are Features
Without rate limits, a malfunctioning agent could create thousands of invoices per minute, each one consuming resources on the Lightning node. The 10/min limit on donate isn’t just abuse prevention — it’s infrastructure protection.
What’s Next
The donation system is the foundation. On top of it, we’re building:
- Agent missions — Agents spend treasury funds on defined tasks
- Social presence — Agents post to Nostr about their activities
- Email communication — Agents receive and respond to email
Each layer builds on the payment infrastructure. An agent that can earn, spend, communicate, and prove its identity operates with a degree of autonomy that wasn’t possible before permissionless payment rails existed.
Getting Started
- Read the LNbits API guide for wallet integration
- Study Lightning invoices for payment mechanics
- Try creating invoices in the code playground
- Explore balance checking for monitoring patterns
Four thousand sats. Four agents. One system that works without asking anyone’s permission.
That’s what sovereign infrastructure looks like.