This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
token-gated-economy/implementation-guide-taproot-assets-l402-fastapi.md

704 lines
29 KiB
Markdown
Raw Normal View History

Task #1: Taproot Assets + L402 Implementation Spike Produced a comprehensive technical implementation guide covering all six research areas from the spike: 1. Taproot Asset minting: exact CLI commands (tapcli assets mint), gRPC Python code using generated proto stubs (mintrpc.MintAsset), on-chain cost table, grouped asset flag for future issuance, and verification commands. LND v0.20 + tapd v0.7 + litd v0.14 versions confirmed. 2. Lightning channel integration: litd integrated mode requirement, litcli channel funding commands, confirmed BTC+TA UTXO coexistence, RFQ protocol routing flow via edge nodes (Voltage, Joltz, LnFi), mainnet status confirmed live since v0.6 (June 2025). 3. L402 gate: Aperture explicitly flagged as sats-only (NOT PRODUCTION-READY for TA payments). Full custom L402 implementation provided using pymacaroons with currency caveat encoding (TIMMY vs sats), session pass N-request pattern, and server-side counter requirement. 4. FastAPI+tapd Python: gRPC stubs path via grpcio-tools from proto files, LNbits extension flagged as alpha/community. Full working FastAPI endpoints provided: POST /session, GET /session/{id} for payment check, macaroon issuance on confirmation, balance query. 5. Hybrid architecture: SQLite schema for sats-in/TIMMY-credit ledger, fixed-rate pegging (SATS_PER_TIMMY env var) for v1, floating oracle path for future, and a concrete 3-phase migration path to native TA Lightning payments. 6. Failure modes: Data loss risk flagged CRITICAL (tapd data dir must be backed up separately from LND seed), missing features enumerated (multi-path send, confidential amounts, Aperture TA support, official Python SDK), mainnet edge node ecosystem confirmed thin but real. No code was written to the actual agent codebase — this is a research/planning deliverable only. Output file: implementation-guide-taproot-assets-l402-fastapi.md
2026-03-18 13:46:38 +00:00
# Implementation Guide: Taproot Assets + L402 Payment Gate on FastAPI/Lightning
**Project:** Timmy Agent
**Date:** March 18, 2026
**Scope:** Concrete architecture and code paths for TIMMY token minting, Lightning channel integration, L402 payment gating, and FastAPI/Python wiring
**Convention:** `⚠️ NOT PRODUCTION-READY` marks anything that is alpha, testnet-only, or has a known breaking limitation. `✅ MAINNET` marks confirmed production-viable paths.
---
## Prerequisites and Stack Versions
| Component | Required Version | Status |
|-----------|-----------------|--------|
| LND | v0.20.0-beta or later | ✅ MAINNET |
| tapd (Taproot Assets daemon) | v0.7.x (Dec 2025) | ✅ MAINNET (with caveats — see §6) |
| litd (Lightning Terminal) | v0.14.x | ✅ MAINNET — required for TA channels |
| Python | 3.11+ | ✅ |
| grpcio / grpcio-tools | 1.62+ | ✅ |
| FastAPI | 0.110+ | ✅ |
**Critical note:** `litd` must be run in **integrated mode** — LND, tapd, and litd run as a single binary. You cannot run tapd standalone alongside a separately managed LND if you want Lightning channel support.
---
## 1. Taproot Asset Minting
### 1.1 LND Compile Requirements
LND must be compiled with the following RPC tags. Pre-built binaries from the official releases include these; if building from source:
```bash
make install tags="signrpc walletrpc chainrpc invoicesrpc"
```
For the full Lightning Terminal stack (recommended), use litd which bundles everything:
```bash
git clone https://github.com/lightninglabs/lightning-terminal.git
cd lightning-terminal
make install
```
### 1.2 Start tapd on Mainnet
```bash
tapd \
--network=mainnet \
--lnd.host=localhost:10009 \
--lnd.macaroonpath=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon \
--lnd.tlspath=~/.lnd/tls.cert \
--rpclisten=127.0.0.1:10029 \
--restlisten=127.0.0.1:8089
```
Or, using `litd` in integrated mode (required for Lightning channel support):
```ini
# lit.conf
lnd.protocol.option-scid-alias=true
lnd.protocol.zero-conf=true
lnd.protocol.simple-taproot-chans=true
lnd.protocol.simple-taproot-overlay-chans=true
lnd.protocol.custom-message=17
lnd.accept-keysend=true
```
```bash
litd --uipassword=<yourpassword>
```
### 1.3 Mint the TIMMY Token (CLI)
**Step 1 — Queue the mint:**
```bash
# Fixed supply (cannot issue more later)
tapcli assets mint \
--type normal \
--name "TIMMY" \
--supply 1000000 \
--decimal_display 0 \
--meta_bytes "TIMMY: one unit of Timmy agent labor credit"
```
Or with a group key to allow future issuance (recommended for a labor credit that may need top-ups):
```bash
# Grouped asset — allows future tranches of TIMMY
tapcli assets mint \
--type normal \
--name "TIMMY" \
--supply 1000000 \
--decimal_display 0 \
--new_grouped_asset \
--meta_bytes "TIMMY: one unit of Timmy agent labor credit"
```
**Step 2 — Finalize (broadcast the Bitcoin transaction):**
```bash
tapcli assets mint finalize
```
This publishes a single on-chain Bitcoin transaction that anchors the entire supply. The entire supply is minted in one transaction. You do not need multiple transactions for different amounts.
**Step 3 — Verify:**
```bash
tapcli assets list
```
Look for `"name": "TIMMY"` in the output. Note the `asset_id` (a 32-byte hash) and, if grouped, the `tweaked_group_key`. Save both — you will need them for all subsequent operations.
```bash
# Verify a specific asset
tapcli assets list --asset_id <hex_asset_id>
```
### 1.4 gRPC Mint Call (Python)
Generate stubs once from the tapd proto files:
```bash
pip install grpcio grpcio-tools
git clone https://github.com/lightninglabs/taproot-assets.git
cd taproot-assets
mkdir -p python_stubs
python -m grpc_tools.protoc \
-I. \
--python_out=./python_stubs \
--grpc_python_out=./python_stubs \
taprpc/*.proto \
mintrpc/*.proto \
assetwalletrpc/*.proto \
rfqrpc/*.proto
```
Mint via Python gRPC:
```python
import grpc
import codecs
from pathlib import Path
# Generated stubs (from proto generation above)
import mintrpc.mint_pb2 as mintrpc
import mintrpc.mint_pb2_grpc as mintstub
TAPD_GRPC = "localhost:10029"
TLS_PATH = Path.home() / ".taproot-assets/tls.cert"
MAC_PATH = Path.home() / ".taproot-assets/data/mainnet/admin.macaroon"
def tapd_channel():
cert = open(TLS_PATH, "rb").read()
mac = codecs.encode(open(MAC_PATH, "rb").read(), "hex")
def _mac_cb(context, callback):
callback([("macaroon", mac)], None)
ssl = grpc.ssl_channel_credentials(cert)
auth = grpc.metadata_call_credentials(_mac_cb)
creds = grpc.composite_channel_credentials(ssl, auth)
return grpc.secure_channel(TAPD_GRPC, creds)
def mint_timmy(supply: int = 1_000_000):
with tapd_channel() as ch:
stub = mintstub.MintStub(ch)
req = mintrpc.MintAssetRequest(
asset=mintrpc.MintAsset(
asset_type=mintrpc.NORMAL, # fungible
name="TIMMY",
asset_meta=mintrpc.AssetMeta(
data=b"TIMMY: one unit of Timmy agent labor credit"
),
amount=supply,
new_grouped_asset=True, # allows future issuance
decimal_display=0,
),
short_response=False,
)
resp = stub.MintAsset(req)
print(resp)
def finalize_mint():
with tapd_channel() as ch:
stub = mintstub.MintStub(ch)
resp = stub.FinalizeBatch(mintrpc.FinalizeBatchRequest())
print(resp)
```
### 1.5 On-Chain Cost
The minting transaction is a standard Bitcoin Taproot transaction. Cost depends entirely on the current fee market:
| Network congestion | Approximate cost |
|-------------------|-----------------|
| Low (<5 sat/vB) | $3$10 |
| Normal (1030 sat/vB) | $10$40 |
| High (>100 sat/vB) | $40$200+ |
The mint is a **single transaction regardless of supply size**. Minting 1 token or 1,000,000 tokens costs the same — it is just a Bitcoin Taproot output with a commitment in the witness.
---
## 2. Taproot Assets on Lightning Channels
### 2.1 Fund a Lightning Channel with TIMMY Tokens
Get your asset's group key first:
```bash
tapcli assets list
# note: tweaked_group_key from the output
```
Open a channel using `litcli` (not `lncli`):
```bash
litcli ln fundchannel \
--node_pubkey <peer_pubkey> \
--local_sat_amount 100000 \
--push_sat 0 \
--asset_id <hex_asset_id> \
--asset_amount 10000
```
The `local_sat_amount` is the BTC reserve deposited alongside the asset — the channel requires both a BTC component and the asset component. This BTC functions as the fee buffer for routing and channel closure.
### 2.2 BTC Channel Coexistence
**Yes, TA channels can coexist alongside BTC channels in the same UTXO.** The protocol specifically supports this: "Taproot Assets channels can be created alongside BTC channels in the same UTXO, allowing Taproot Assets to exist in the Lightning Network without consuming additional resources."
A single channel funding transaction can carry both BTC and Taproot Asset commitments. This means no extra on-chain transaction overhead once you are already opening a channel with a peer.
### 2.3 Routing: How TIMMY Payments Route Over Lightning
The RFQ (Request for Quote) protocol, live on mainnet since **v0.6 (June 2025)**, is the routing mechanism:
```
[User: holds TIMMY in TA channel with Timmy node]
▼ TA channel
[Timmy Node / Edge Node]
│ RFQ: TIMMY → sats (rate locked, time-limited)
▼ Standard BTC Lightning
[Rest of Lightning Network]
```
**When no direct TIMMY-to-TIMMY route exists (which is almost always the case initially):**
- Edge nodes perform the swap automatically. The edge node holds TIMMY in a private TA channel to the user and BTC channels to the rest of the network.
- The swap is atomic via HTLC. The user's TIMMY is debited, the recipient receives BTC sats (or another TA asset if they also have an edge node).
- **This means TIMMY can pay any standard Lightning invoice**, even nodes that have never heard of Taproot Assets, as long as an edge node is in the path.
**v0.7 improvement:** AddressV2 (static, reusable receive addresses) simplifies the receive side — users don't need to generate a fresh address per invoice.
### 2.4 Current LND v0.20+ TA Channel Status
**Lightning channel support is live on mainnet as of v0.6 (June 2025) / v0.7 (December 2025).**
Production edge nodes confirmed operating as of early 2026: Voltage, Amboss, Joltz, LnFi Network, Speed.
⚠️ **Multi-path sending** (splitting an outbound TIMMY payment across multiple channels) is **not yet implemented** in v0.7. Multi-path receive was added in v0.6 (up to 20 inbound channels). Multi-path send is planned for a future release.
---
## 3. L402 Payment Gate with Taproot Assets
### 3.1 Can Aperture Gate Using TIMMY Tokens?
⚠️ **NOT PRODUCTION-READY.** As of March 2026, Aperture (Lightning Labs' L402 reverse proxy) natively supports **BTC satoshi payments only**. It generates standard BOLT-11 Lightning invoices and validates payment via preimage. There is no built-in Aperture support for Taproot Asset invoices.
**Interim path: build a thin custom L402 layer on top of tapd.**
### 3.2 Custom L402 Implementation for Taproot Asset Payments
The standard L402 flow adapted for TIMMY:
1. Client hits protected endpoint
2. Server generates a tapd TA address (AddressV2) for `N` TIMMY tokens — this is the "invoice"
3. Server returns `HTTP 402` with the TA address and a pending macaroon
4. Client sends TIMMY to the address
5. Server polls tapd for incoming transfer confirmation, then issues the activated macaroon
6. Client includes `Authorization: L402 <macaroon>:<transfer_txid>` on the next request
The key difference from standard L402: Lightning BOLT-11 invoices confirm in 13 seconds with an atomic preimage. Taproot Asset on-chain transfers confirm in ~10 minutes (1 block). **For the eval-fee pattern, this means on-chain TA transfers are not usable for real-time gating.** TA Lightning channel payments (off-chain) are the only viable path for sub-second confirmation.
For TA Lightning channel payments, the flow uses `tapcli assets send --addr <ta_address>` or the equivalent gRPC call, which generates an off-chain HTLC that settles in 13 seconds, matching standard Lightning behavior.
### 3.3 Macaroon Caveat Encoding for TIMMY vs Sats
Macaroons are issued by your server, not by LND. Add a caveat identifying the payment currency:
```python
import pymacaroons # pip install pymacaroons
def issue_session_macaroon(
root_key: bytes,
user_id: str,
payment_hash: str,
currency: str, # "TIMMY" or "sats"
amount: int,
max_requests: int,
expires_at: str, # ISO 8601
) -> str:
m = pymacaroons.Macaroon(
location="https://timmy.agent",
identifier=f"user:{user_id}:ph:{payment_hash}",
key=root_key,
)
m.add_first_party_caveat(f"currency = {currency}")
m.add_first_party_caveat(f"amount_paid = {amount}")
m.add_first_party_caveat(f"max_requests = {max_requests}")
m.add_first_party_caveat(f"expires_at = {expires_at}")
m.add_first_party_caveat(f"payment_hash = {payment_hash}")
return m.serialize()
def verify_session_macaroon(
root_key: bytes,
token: str,
requests_used: int,
) -> bool:
v = pymacaroons.Verifier()
v.satisfy_exact("currency = TIMMY")
v.satisfy_exact("currency = sats")
v.satisfy_general(lambda c: c.startswith("amount_paid = "))
v.satisfy_general(lambda c: c.startswith("payment_hash = "))
v.satisfy_general(lambda c: _check_requests(c, requests_used))
v.satisfy_general(lambda c: _check_expiry(c))
m = pymacaroons.Macaroon.deserialize(token)
try:
v.verify(m, root_key)
return True
except Exception:
return False
def _check_requests(caveat: str, used: int) -> bool:
if caveat.startswith("max_requests = "):
max_r = int(caveat.split(" = ")[1])
return used < max_r
return True
def _check_expiry(caveat: str) -> bool:
from datetime import datetime, timezone
if caveat.startswith("expires_at = "):
exp = datetime.fromisoformat(caveat.split(" = ")[1])
return datetime.now(timezone.utc) < exp
return True
```
### 3.4 Session Pass Pattern (Prepay N Requests)
The session pass flow using TIMMY:
1. User calls `POST /session` with desired number of requests (e.g., 10)
2. Server returns a TA address for `N × cost_per_request` TIMMY tokens
3. User pays via TA Lightning channel (off-chain, ~2s)
4. Server confirms payment, issues a macaroon with `max_requests = 10`
5. User submits requests with the macaroon in the `Authorization` header
6. Server decrements a server-side counter per request (the `max_requests` caveat is a hint, server-side tracking is authoritative)
⚠️ The `max_requests` caveat cannot enforce itself — macaroons are bearer tokens, not stateful counters. The server **must** maintain a counter in its database keyed on the payment hash. The caveat is a hint for the client; the server enforces the limit.
---
## 4. FastAPI Integration
### 4.1 Python Path to tapd: gRPC vs REST vs LNbits
| Path | Maturity | When to use |
|------|----------|-------------|
| **gRPC stubs (generated from .proto)** | ✅ Official, documented | Production; full access to all tapd APIs |
| **tapd REST API (port 8089)** | ✅ Works today | Simpler integration; slightly less coverage than gRPC |
| **LNbits Taproot Assets extension** | ⚠️ Community; alpha | Rapid prototyping only; not battle-tested for production |
**Recommendation:** Use the tapd REST API for simplicity during development; migrate to gRPC for production to access features like RFQ negotiation that are gRPC-only.
### 4.2 LNbits Taproot Assets Extension Status (March 2026)
The community extension (`echennells/taproot_assets`) exists and connects LNbits to `litd` via gRPC. It supports asset listing, send/receive, channel viewing, and balance tracking with WebSocket updates. It bundles its own LND and tapd protobuf stubs (`lnd_grpc_files.tar.gz`, `tapd_grpc_files.tar.gz`).
⚠️ **Production status: Alpha/community.** Use it for local development and testing. For a production service handling real funds, wire FastAPI directly to tapd gRPC.
### 4.3 Minimal FastAPI Endpoint: Create TA Address, Check Payment, Issue Macaroon
```python
import asyncio
import codecs
import os
import secrets
from datetime import datetime, timedelta, timezone
from pathlib import Path
import grpc
import httpx
import pymacaroons
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
# ── Generated stubs ─────────────────────────────────────────────────────────
# Generate with: python -m grpc_tools.protoc -I. --python_out=./stubs
# --grpc_python_out=./stubs taprpc/*.proto
import stubs.taprootassets_pb2 as taprpc
import stubs.taprootassets_pb2_grpc as tapstub
app = FastAPI()
TAPD_GRPC = os.getenv("TAPD_GRPC_HOST", "localhost:10029")
TAPD_TLS = Path(os.getenv("TAPD_TLS_PATH", "~/.taproot-assets/tls.cert")).expanduser()
TAPD_MAC = Path(os.getenv("TAPD_MAC_PATH", "~/.taproot-assets/data/mainnet/admin.macaroon")).expanduser()
TIMMY_ID = bytes.fromhex(os.getenv("TIMMY_ASSET_ID", "")) # 32-byte asset id hex
MACAROON_ROOT_KEY = bytes.fromhex(os.getenv("MACAROON_ROOT_KEY", secrets.token_hex(32)))
# ── tapd gRPC channel ────────────────────────────────────────────────────────
def _tapd_creds():
cert = TAPD_TLS.read_bytes()
mac = codecs.encode(TAPD_MAC.read_bytes(), "hex")
def _cb(context, callback):
callback([("macaroon", mac)], None)
return grpc.composite_channel_credentials(
grpc.ssl_channel_credentials(cert),
grpc.metadata_call_credentials(_cb),
)
_channel = grpc.secure_channel(TAPD_GRPC, _tapd_creds())
_tap = tapstub.TaprootAssetsStub(_channel)
# ── In-memory session store (replace with SQLite/Redis in prod) ──────────────
_sessions: dict[str, dict] = {}
# ── Models ───────────────────────────────────────────────────────────────────
class SessionRequest(BaseModel):
num_requests: int = 10
class SessionResponse(BaseModel):
ta_address: str # user sends TIMMY here
timmy_amount: int # how many TIMMY to send
session_id: str # poll /session/{session_id} to check payment
class SessionStatus(BaseModel):
paid: bool
macaroon: str | None = None
# ── Helpers ──────────────────────────────────────────────────────────────────
TIMMY_PER_REQUEST = int(os.getenv("TIMMY_PER_REQUEST", "1"))
def _new_ta_address(amount: int) -> str:
"""Create a tapd AddressV2 to receive `amount` TIMMY tokens."""
req = taprpc.NewAddrRequest(
asset_id=TIMMY_ID,
amt=amount,
address_version=taprpc.ADDR_VERSION_V2,
)
resp = _tap.NewAddr(req)
return resp.encoded # bech32m ta1... address
def _check_recent_receives(ta_address: str) -> bool:
"""Check if TIMMY has been received to the given address."""
req = taprpc.ListTransfersRequest()
resp = _tap.ListTransfers(req)
for transfer in resp.transfers:
for output in transfer.outputs:
if output.address == ta_address and output.status == taprpc.OUTPUT_STATUS_COMPLETED:
return True
return False
def _issue_macaroon(session_id: str, num_requests: int) -> str:
expires = (datetime.now(timezone.utc) + timedelta(hours=24)).isoformat()
return issue_session_macaroon(
root_key=MACAROON_ROOT_KEY,
user_id=session_id,
payment_hash=session_id,
currency="TIMMY",
amount=num_requests * TIMMY_PER_REQUEST,
max_requests=num_requests,
expires_at=expires,
)
# Reuse the macaroon helpers from §3.3
def issue_session_macaroon(root_key, user_id, payment_hash,
currency, amount, max_requests, expires_at) -> str:
m = pymacaroons.Macaroon(
location="https://timmy.agent",
identifier=f"user:{user_id}:ph:{payment_hash}",
key=root_key,
)
m.add_first_party_caveat(f"currency = {currency}")
m.add_first_party_caveat(f"amount_paid = {amount}")
m.add_first_party_caveat(f"max_requests = {max_requests}")
m.add_first_party_caveat(f"expires_at = {expires_at}")
return m.serialize()
# ── Routes ───────────────────────────────────────────────────────────────────
@app.post("/session", response_model=SessionResponse)
async def create_session(req: SessionRequest):
if req.num_requests < 1 or req.num_requests > 100:
raise HTTPException(400, "num_requests must be 1100")
timmy_amount = req.num_requests * TIMMY_PER_REQUEST
ta_address = _new_ta_address(timmy_amount)
session_id = secrets.token_hex(16)
_sessions[session_id] = {
"ta_address": ta_address,
"timmy_amount": timmy_amount,
"num_requests": req.num_requests,
"paid": False,
"requests_used": 0,
}
return SessionResponse(
ta_address=ta_address,
timmy_amount=timmy_amount,
session_id=session_id,
)
@app.get("/session/{session_id}", response_model=SessionStatus)
async def check_session(session_id: str):
s = _sessions.get(session_id)
if not s:
raise HTTPException(404, "Session not found")
if not s["paid"]:
# Poll tapd for incoming transfer
paid = await asyncio.to_thread(_check_recent_receives, s["ta_address"])
if paid:
s["paid"] = True
s["macaroon"] = _issue_macaroon(session_id, s["num_requests"])
return SessionStatus(
paid=s["paid"],
macaroon=s.get("macaroon"),
)
@app.get("/session/{session_id}/balance")
async def query_timmy_balance(session_id: str):
"""
Query TIMMY balance held by this node across all channels/UTXOs.
Replace asset_id filter with your TIMMY_ID.
"""
req = taprpc.ListBalancesRequest(asset_id=TIMMY_ID)
resp = _tap.ListBalances(req)
return {"balance": resp.asset_balances.get(TIMMY_ID.hex(), {}).get("balance", 0)}
```
### 4.4 Query a User's TIMMY Balance
To check how many TIMMY a **remote address** (user) holds, query the tapd universe — the public registry of asset ownership proofs:
```bash
# Check universe for asset transfers associated with an address
tapcli universe leaves --asset_id <hex_asset_id> --proof_type issuance
```
For an application-level balance check (tracking credits against a session), maintain the balance in your own database. tapd does not expose third-party address balances — only your own node's holdings.
---
## 5. Hybrid Architecture: Sats + TIMMY Credits
### 5.1 Why the Hybrid Is Necessary Now
⚠️ **As of March 2026**, the full Taproot Asset Lightning stack requires running `litd` in integrated mode with LND + tapd. This is non-trivial for a solo developer shipping a first version. Additionally, multi-path send is missing, and the alpha warnings are real (see §6).
The practical production path today:
- **Users pay in sats** via standard Lightning (BOLT-11 invoice, LNbits, any wallet)
- **Internal ledger tracks TIMMY credits** in your database
- TIMMY credits are not on-chain tokens yet — they are your application's internal accounting unit
This lets you ship the payment gate today using standard, battle-tested Lightning tooling.
### 5.2 Internal Credit Ledger Model
```sql
-- Minimal schema (SQLite or PostgreSQL)
CREATE TABLE sessions (
id TEXT PRIMARY KEY, -- session UUID
user_id TEXT NOT NULL,
currency TEXT NOT NULL, -- 'sats' or 'TIMMY'
amount_paid INTEGER NOT NULL, -- sats paid or TIMMY credited
timmy_credits INTEGER NOT NULL, -- TIMMY credit balance
requests_used INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
expires_at TEXT NOT NULL,
paid INTEGER DEFAULT 0 -- 0=pending, 1=confirmed
);
CREATE TABLE requests (
id TEXT PRIMARY KEY,
session_id TEXT REFERENCES sessions(id),
eval_fee_paid INTEGER DEFAULT 0, -- 1=eval invoice paid
work_fee_paid INTEGER DEFAULT 0, -- 1=work invoice paid
timmy_debited INTEGER DEFAULT 0, -- TIMMY credits consumed
created_at TEXT DEFAULT (datetime('now'))
);
```
### 5.3 TIMMY Credit Pegging Strategy
**Option A: Fixed operator rate (recommended for v1)**
Set a fixed rate at deployment:
```python
SATS_PER_TIMMY = int(os.getenv("SATS_PER_TIMMY", "1000")) # 1 TIMMY = 1000 sats
```
- Operator controls the rate via env var
- Simple, predictable for users
- Adjust the rate when relaunching; existing sessions keep their purchased credits
- Risk: if sat/USD moves significantly, the labor price in USD drifts
**Option B: Floating rate (future)**
Derive `SATS_PER_TIMMY` from a price oracle (e.g., CoinGecko USD/BTC feed) to keep TIMMY's labor value stable in USD terms. Implement this only after the fixed-rate model is validated.
### 5.4 Migration Path to Native TA Payments
When you are ready to move from the sats + internal ledger hybrid to native Taproot Asset Lightning payments:
**Phase 1 (current):** Sats in → internal TIMMY credits (database)
**Phase 2 (when TA Lightning is stable for your use case):**
1. Deploy `litd` in integrated mode alongside the FastAPI server
2. Mint the TIMMY token on-chain (§1 above)
3. Keep the internal credit ledger — it becomes a bridge between sats-paying users and token-holding users
4. Add a new endpoint: `POST /redeem` — user sends on-chain TIMMY to your tapd address; server credits their account in the ledger
5. Users who prefer sats continue using the existing flow; no migration required for them
**Phase 3 (full native):**
1. Open TIMMY TA Lightning channels with an edge node (Voltage, Joltz, or LnFi)
2. Accept TIMMY payments directly over Lightning without touching sats
3. Retire the sat-based payment endpoint or keep it as a legacy path
4. The `sessions` table gains a `native_ta` column; macaroons encode `currency = TIMMY` (native) vs `currency = sats` (legacy credit)
The database schema is the same across all phases. The payment confirmation logic changes (LNbits sat invoice → tapd TA transfer), but the session/macaroon layer is untouched.
---
## 6. What Breaks
### 6.1 Known Failure Modes (March 2026)
**⚠️ ALPHA — FUNDS AT RISK**
The official tapd documentation states verbatim: *"there can still be bugs and not all desired data safety and backup mechanisms have been implemented yet."*
| Failure mode | Severity | Detail |
|-------------|----------|--------|
| **Local data loss → asset destruction** | CRITICAL | If the tapd data directory is lost without a proper backup, assets are permanently destroyed. The LND seed phrase alone is NOT sufficient to restore Taproot Assets. `tapd` holds the taproot tweak separately from `lnd`'s key; both are required. |
| **Uninstalling litd without backup** | CRITICAL | Applies to Umbrel and similar node-in-a-box setups. Uninstall deletes application data, destroying assets. |
| **No confidential asset amounts** | HIGH | All asset balances in TA channels are public to channel peers. Planned but not implemented. |
| **No smart contracts** | MEDIUM | TA channels support payments only. No programmable conditions (escrow, time-locks beyond channel mechanics). |
| **Edge node requirement** | MEDIUM | Sending TIMMY to a non-TA Lightning node requires routing through an edge node. If no edge node is in the path, payment fails. With limited edge node availability (Voltage, Joltz, LnFi), routing reliability depends on a small set of operators. |
| **Multi-path send not implemented** | MEDIUM | Large TIMMY sends cannot be split across multiple outbound channels. Only single-path outbound. Multi-path receive works (up to 20 channels). |
| **Forced litd integrated mode** | LOW-MEDIUM | You cannot run tapd as a standalone sidecar next to a pre-existing LND. You must run litd. This is an ops constraint, not a protocol flaw. |
### 6.2 What Is NOT Yet Implemented in tapd (v0.7)
| Feature | Status |
|---------|--------|
| Multi-path sending (outbound) | Planned, not in v0.7 |
| Confidential asset amounts | Planned |
| Full smart contract support | Not planned for TA (protocol constraint) |
| Native Aperture L402 support for TA payments | Not implemented in Aperture |
| Python SDK (official) | Not available — use gRPC stubs or REST |
| tapd standalone mode (without litd) for Lightning channels | Not supported |
### 6.3 Mainnet TA Channels: Production or Testnet?
**Mainnet production as of v0.6 (June 2025).** Confirmed operators: Voltage, Amboss, Joltz, Speed, LnFi Network.
⚠️ The production ecosystem is small. As of March 2026, the Taproot Assets Lightning channel network is real but thin. Routing reliability is lower than the broader BTC Lightning Network. Do not assume your TIMMY payment will route successfully without a direct edge node relationship.
### 6.4 Protocol Evolution Risk
The Lightning Labs team declared **forward compatibility** starting with the v0.3 mainnet alpha (Oct 2023): assets issued on mainnet will not be broken by future protocol versions. The protocol API and message formats, however, are still stabilizing. gRPC stubs generated from v0.7 proto files may need regeneration for v0.8+.
**Mitigation:** Abstract all tapd calls behind a single `TapdService` class. When the proto changes, update the stubs and the service class in one place. The FastAPI routes and business logic remain stable.
---
## Summary: What Works Today vs What to Wait On
| Feature | Today's path | Wait for |
|---------|-------------|----------|
| Mint TIMMY on-chain | ✅ `tapcli assets mint` — single tx, mainnet | — |
| TIMMY in Lightning channels | ✅ `litd` integrated mode, v0.7 | — |
| BTC + TIMMY in same UTXO | ✅ Supported | — |
| Route TIMMY over BTC Lightning | ✅ Via edge nodes (Voltage, Joltz) | Multi-path send |
| L402 gate with TIMMY | ⚠️ Custom implementation required | Native Aperture TA support |
| Session pass macaroon (N requests) | ✅ `pymacaroons` + server-side counter | — |
| FastAPI + tapd Python | ✅ gRPC stubs from proto files | Official Python SDK |
| LNbits TA extension | ⚠️ Alpha/community | Stable release |
| Hybrid sats → TIMMY credit ledger | ✅ Standard Lightning + SQLite | — |
| Migration to native TA payments | ✅ Architecture is clear | Multi-path send stabilization |
**Recommended starting point:** Implement the hybrid architecture (§5) using LNbits for sats payments and an internal TIMMY credit ledger. Wire tapd for the on-chain mint only (§1). Add native TA Lightning payments (§24) once the edge node ecosystem matures further and you have validated demand.
---
*Sources: Lightning Labs official tapd documentation and API reference (lightning.engineering), tapd GitHub repository (v0.7), LND v0.20 release notes, Taproot Assets Builder's Guide, echennells/taproot_assets LNbits extension, pymacaroons library documentation, L402 specification (lightning.engineering/posts), Voltage/Joltz/LnFi edge node announcements.*