# Implementation Guide: Taproot Assets + L402 Payment Gate on FastAPI/Lightning

**Project:** Timmy Agent  
**Date:** March 18, 2026  
**Validated against:** tapd v0.7.x / litd v0.14.x / LND v0.20.0-beta (December 2025 release cycle)  
**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.

> **Proto field caveat:** gRPC stubs must be regenerated from the `.proto` files of each tapd release. Field names and message structures may change between minor versions. Always run `python -m grpc_tools.protoc` against the proto files from the exact tapd version you are running, and re-validate field names in the Python snippets below against your generated stubs before production use.

---

## 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 (10–30 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 1–3 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 1–3 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()

ALLOWED_CURRENCIES = {"TIMMY", "sats"}

def verify_session_macaroon(
    root_key: bytes,
    token: str,
    requests_used: int,
) -> bool:
    v = pymacaroons.Verifier()
    # Allow exactly one currency caveat per token — use a general checker, NOT
    # multiple satisfy_exact() calls. satisfy_exact() requires ALL listed values
    # to appear, which would fail any single-currency token.
    v.satisfy_general(lambda c: (
        c.startswith("currency = ") and c.split(" = ")[1] in ALLOWED_CURRENCIES
    ))
    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.

**REST curl quick reference — tapd v0.7 (port 8089):**

```bash
# Set these once
TAPD_MAC_HEX=$(xxd -p ~/.taproot-assets/data/mainnet/admin.macaroon | tr -d '\n')

# List all assets held by this node
curl -s --cacert ~/.taproot-assets/tls.cert \
  -H "Grpc-Metadata-macaroon: $TAPD_MAC_HEX" \
  https://localhost:8089/v1/taproot-assets/assets | jq .

# Create a TIMMY receive address (AddressV2)
# Replace <base64_asset_id> with your TIMMY asset ID encoded as base64
curl -s --cacert ~/.taproot-assets/tls.cert \
  -H "Grpc-Metadata-macaroon: $TAPD_MAC_HEX" \
  -H "Content-Type: application/json" \
  -X POST https://localhost:8089/v1/taproot-assets/addrs \
  -d '{
        "asset_id": "<base64_asset_id>",
        "amt": "10",
        "address_version": "ADDR_VERSION_V2"
      }' | jq .encoded

# Check recent transfers (to detect incoming TIMMY payment)
curl -s --cacert ~/.taproot-assets/tls.cert \
  -H "Grpc-Metadata-macaroon: $TAPD_MAC_HEX" \
  https://localhost:8089/v1/taproot-assets/transfers | jq .

# Query TIMMY balance
curl -s --cacert ~/.taproot-assets/tls.cert \
  -H "Grpc-Metadata-macaroon: $TAPD_MAC_HEX" \
  "https://localhost:8089/v1/taproot-assets/assets/balance?asset_id_filter=<hex_asset_id>" | jq .
```

### 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)))
# ⚠️ PRODUCTION REQUIREMENT: MACAROON_ROOT_KEY must be a stable, persistent secret stored in an
# environment variable or secrets manager. The default fallback (secrets.token_hex) generates a
# new random key on every process restart, which INVALIDATES ALL PREVIOUSLY ISSUED MACAROONS.
# Set MACAROON_ROOT_KEY once at deployment and never rotate it without invalidating active sessions.

# ── 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 1–100")
    
    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 (§2–4) once the edge node ecosystem matures further and you have validated demand.

---

## References

| Claim | Source | Date |
|-------|--------|------|
| LND v0.20 required for tapd v0.7 | [Lightning Labs API Docs — tapd](https://lightning.engineering/api-docs/api/taproot-assets/) | Dec 2025 |
| tapd v0.7 mainnet release | [tapd GitHub releases](https://github.com/lightninglabs/taproot-assets/releases) | Dec 2025 |
| TA Lightning channel support live mainnet v0.6 | [Lightning Labs blog — Taproot Assets v0.6](https://lightning.engineering/posts/2025-06-taproot-assets-v0-6/) | Jun 2025 |
| BTC + TA channels in same UTXO | [Taproot Assets Builder's Guide](https://docs.lightning.engineering/lightning-network-tools/taproot-assets) | 2025 |
| RFQ protocol mainnet, multi-path receive (20 channels) | tapd v0.6 release notes | Jun 2025 |
| AddressV2 static reusable addresses | tapd v0.7 release notes | Dec 2025 |
| Edge node operators: Voltage, Amboss, Joltz, Speed, LnFi | [Voltage blog](https://voltage.cloud), [Joltz docs](https://joltz.app) | 2025–2026 |
| Aperture L402 sats-only (no native TA payment support) | [Aperture GitHub](https://github.com/lightninglabs/aperture) — no TA payment backend | Mar 2026 |
| LNbits TA extension (echennells) | [echennells/taproot_assets GitHub](https://github.com/echennells/taproot_assets) | 2025 |
| Data loss warning — LND seed insufficient for TA restore | [tapd official docs safety warning](https://docs.lightning.engineering/lightning-network-tools/taproot-assets) | 2025 |
| Multi-path send not implemented in v0.7 | tapd v0.7 release notes / GitHub milestones | Dec 2025 |
| Alpha software warning, bugs and missing backup mechanisms | [tapd docs](https://docs.lightning.engineering/lightning-network-tools/taproot-assets) | 2025 |
| pymacaroons library | [pymacaroons PyPI](https://pypi.org/project/pymacaroons/) | — |
| L402 specification | [lightning.engineering/posts/2023-06-07-l402](https://lightning.engineering/posts/2023-06-07-l402/) | Jun 2023 |
| LangChainBitcoin L402 Python | [lightninglabs/LangChainBitcoin](https://github.com/lightninglabs/LangChainBitcoin) | 2023 |
