4 changes to address code review rejections:
1. Race condition fix (bootstrap.ts)
- advanceBootstrapJob: WHERE now guards on AND state='awaiting_payment'
- If UPDATE matches 0 rows, re-fetch current job (already advanced by
another concurrent poll) instead of firing a second provisioner
- Verified with 5-concurrent-poll test: only 1 "starting provisioning"
log entry per job; all 5 responses show consistent state
2. Complete cloud-init to full Bitcoin + LND + LNbits stack (provisioner.ts)
- Phase 1: packages, Docker, Tailscale, UFW, block volume mount
- Phase 2: Bitcoin Core started; polls for RPC availability (max 5 min)
- Phase 3: LND started; waits for REST API (max 6 min)
- Phase 4: non-interactive LND wallet init via REST:
POST /v1/genseed → POST /v1/initwallet with base64 password
(no lncli, no interactive prompts, no expect)
- Phase 5: waits for admin.macaroon to appear on mounted volume
- Phase 6: LNbits started with LndRestWallet backend; mounts LND
data dir so it reads tls.cert + admin.macaroon automatically
- Phase 7: saves all credentials (RPC pass, LND wallet pass + seed
mnemonic, LNbits URL) to chmod 600 /root/node-credentials.txt
3. DO block volume support (provisioner.ts)
- Reads DO_VOLUME_SIZE_GB env var (0 = no volume, default)
- createVolume(): POST /v2/volumes (ext4 filesystem, tagged timmy-node)
- Passes volumeId in droplet create payload (attached at boot)
- Cloud-init Phase 1 detects and mounts the volume automatically
(lsblk scan → mkfs if unformatted → mount → /etc/fstab entry)
4. SSH keypair via node:crypto (no ssh-keygen) (provisioner.ts)
- generateKeyPairSync('rsa', { modulusLength: 4096 })
- Public key: PKCS#1 DER → OpenSSH wire format via manual DER parser
(pkcs1DerToSshPublicKey): reads SEQUENCE → n, e INTEGERs → ssh-rsa
base64 string with proper mpint encoding (leading 0x00 for high bit)
- Private key: PKCS#1 PEM (-----BEGIN RSA PRIVATE KEY-----)
- Both stub and real paths use the same generateSshKeypair() function
- Removes runtime dependency on host ssh-keygen binary entirely
Pay a Lightning invoice → Timmy auto-provisions a Bitcoin full node on DO.
New: lib/db/src/schema/bootstrap-jobs.ts
- bootstrap_jobs table: id, state, amountSats, paymentHash, paymentRequest,
dropletId, nodeIp, tailscaleHostname, lnbitsUrl, sshPrivateKey,
sshKeyDelivered (bool), errorMessage, createdAt, updatedAt
- States: awaiting_payment | provisioning | ready | failed
- Payment data stored inline (no FK to jobs/invoices tables — separate entity)
- db:push applied to create table in Postgres
New: artifacts/api-server/src/lib/provisioner.ts
- ProvisionerService: stubs when DO_API_TOKEN absent, real otherwise
- Stub mode: generates a real RSA 4096-bit SSH keypair via ssh-keygen,
returns RFC 5737 test IP + fake Tailscale hostname after 2s delay
- Real mode: upload SSH public key to DO → generate Tailscale auth key →
create DO droplet with cloud-init user_data → poll for public IP (2 min)
- buildCloudInitScript(): non-interactive bash that installs Docker + Tailscale
+ UFW + Bitcoin Knots via docker-compose; joins Tailscale if authkey provided
- provision() designed as fire-and-forget (void); updates DB to ready/failed
New: artifacts/api-server/src/routes/bootstrap.ts
- POST /api/bootstrap: create job + LNbits invoice, return paymentRequest
- GET /api/bootstrap/🆔 poll-driven state machine
* awaiting_payment: checks payment, fires provisioner on confirm
* provisioning: returns progress message
* ready: delivers credentials; SSH private key delivered once then cleared
* failed: returns error message
- Stub mode message includes the exact /dev/stub/pay URL for easy testing
- nextSteps array guides user through post-provision setup
Updated: artifacts/api-server/src/lib/pricing.ts
- Added bootstrapFee field reading BOOTSTRAP_FEE_SATS env var (default 10000)
- calculateBootstrapFeeSats() method
Updated: artifacts/api-server/src/routes/index.ts
- Mounts bootstrapRouter
Updated: replit.md
- Documents all 7 new env vars (DO_API_TOKEN, DO_REGION, DO_SIZE, etc.)
- Full curl-based flow example with annotated response shape
End-to-end verified in stub mode: POST → pay → provisioning → ready (SSH key)
→ second GET clears key and shows sshKeyNote
Integrate Anthropic AI for agent capabilities, introduce database schemas for jobs and invoices, and set up LNbits for payment processing.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: cce28acc-aeac-46ff-80ec-af4ade39e30f
Replit-Helium-Checkpoint-Created: true