Skip to content

Latest commit

 

History

History
1028 lines (832 loc) · 27.9 KB

File metadata and controls

1028 lines (832 loc) · 27.9 KB

Routstr Integration Plan for OpenCode

Executive Summary

This document outlines a comprehensive 3-stage plan to integrate Routstr (a Bitcoin eCash payment proxy for AI APIs) into OpenCode. The integration will enable users to pay for AI model requests using Bitcoin Lightning Network payments through Cashu eCash tokens, providing anonymous, instant micropayments without requiring traditional payment methods.

Background

Current OpenCode Authentication System

  • Auth storage: /Users/{user}/.opencode/auth.json (600 permissions)
  • Auth types: OAuth, API key, WellKnown
  • Provider system: Dynamic provider loading from models.dev API
  • CLI integration: opencode auth login command with provider selection
  • TUI integration: Go-based terminal UI with status bar

Routstr Architecture

  • Payment model: Pre-funded accounts using Cashu eCash tokens
  • API compatibility: Full OpenAI API compatibility
  • Token usage: Two flows - persistent API keys or direct token usage
  • Cost calculation: Per-token billing with BTC/USD conversion
  • Refund system: Unused balance can be withdrawn as eCash tokens

Key Challenges

  1. Token lifecycle management: Cashu tokens are spent/consumed on use
  2. Balance tracking: Real-time balance monitoring and low-balance handling
  3. Top-up workflow: Seamless refill experience
  4. Status bar integration: Show wallet balance and payment status
  5. Refund handling: Withdraw unused funds

Three Integration Approaches

Based on the Routstr documentation and system architecture, here are three different approaches for handling token management and user experience:

Approach A: Persistent Wallet Model (Recommended)

Philosophy: Create persistent API keys backed by Cashu wallet, similar to traditional API key workflow.

Flow:

  1. User provides Cashu token during opencode auth login
  2. OpenCode calls Routstr /v1/wallet/create to create persistent API key
  3. Store the returned API key (sk-...) in auth.json like traditional providers
  4. Use standard API key flow for all requests
  5. Implement background balance checking and top-up prompts
  6. Support refund withdrawals through wallet commands

Pros:

  • Familiar user experience (like existing API keys)
  • Persistent sessions across restarts
  • Clean separation between payment and request logic
  • Easy to implement existing provider patterns

Cons:

  • Requires additional wallet management endpoints
  • More complex state management

Approach B: Direct Token Usage Model

Philosophy: Use Cashu tokens directly as API keys, rotating tokens as needed.

Flow:

  1. User provides Cashu token during opencode auth login
  2. Store token directly as API key in auth.json
  3. Use token as Authorization header for each request
  4. Handle insufficient balance errors by prompting for new token
  5. Automatically handle refunds and token rotation

Pros:

  • Simpler implementation (no additional endpoints needed)
  • Direct token-to-request mapping
  • Immediate feedback on token exhaustion

Cons:

  • Requires token rotation logic
  • Less predictable user experience
  • Token management complexity

Approach C: Hybrid Wallet Model

Philosophy: Combine persistent wallet with automatic token management.

Flow:

  1. Create persistent wallet ID but manage tokens internally
  2. Automatic token rotation and balance aggregation
  3. Show aggregated balance across multiple tokens
  4. Seamless top-up without user intervention for small amounts

Pros:

  • Best user experience
  • Automatic token management
  • Persistent identity with flexible payments

Cons:

  • Most complex implementation
  • Requires sophisticated token juggling logic

Recommended Approach: A - Persistent Wallet Model

After analyzing the trade-offs, Approach A provides the best balance of implementation complexity and user experience. It leverages existing OpenCode patterns while providing a clean abstraction over the underlying payment complexity.

Implementation Plan

Stage 1: Basic Integration (Get it Working)

Goals

  • Add Routstr as a new provider option in opencode auth login
  • Support basic API requests with Cashu token authentication
  • Minimal viable product for testing

Implementation Steps

1.1 Add Routstr to Auth Provider List

File: /packages/opencode/src/cli/cmd/auth.ts

Add Routstr to the provider priority list:

const priority: Record<string, number> = {
  anthropic: 0,
  "github-copilot": 1,
  openai: 2,
  google: 3,
  routstr: 4,  // Add here
  openrouter: 5,
  vercel: 6,
}

1.2 Create Routstr Auth Handler

File: /packages/opencode/src/auth/routstr.ts

export namespace RoutstrAuth {
  export async function createWallet(cashuToken: string, baseUrl: string): Promise<{
    apiKey: string;
    balance: number;
  }> {
    const response = await fetch(`${baseUrl}/v1/wallet/create`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ cashu_token: cashuToken }),
    });
    
    if (!response.ok) {
      throw new Error(`Failed to create wallet: ${response.status}`);
    }
    
    return response.json();
  }
  
  export async function checkBalance(apiKey: string, baseUrl: string): Promise<{
    balance: number;
    total_deposited: number;
    total_spent: number;
  }> {
    const response = await fetch(`${baseUrl}/v1/wallet/balance`, {
      headers: { 'Authorization': `Bearer ${apiKey}` },
    });
    
    return response.json();
  }
}

1.3 Extend Auth Command for Routstr

File: /packages/opencode/src/cli/cmd/auth.ts

Add special handling in AuthLoginCommand:

if (provider === "routstr") {
  const baseUrl = await prompts.text({
    message: "Enter Routstr base URL",
    initial: "https://api.routstr.com",
  });
  if (prompts.isCancel(baseUrl)) throw new UI.CancelledError();
  
  const cashuToken = await prompts.password({
    message: "Paste your Cashu token",
    validate: (x) => x?.startsWith("cashu") ? undefined : "Must be a valid Cashu token",
  });
  if (prompts.isCancel(cashuToken)) throw new UI.CancelledError();
  
  const spinner = prompts.spinner();
  spinner.start("Creating wallet...");
  
  try {
    const wallet = await RoutstrAuth.createWallet(cashuToken, baseUrl);
    
    await Auth.set("routstr", {
      type: "api",
      key: wallet.apiKey,
    });
    
    // Store additional metadata
    await Auth.set("routstr-meta", {
      type: "api", 
      key: JSON.stringify({
        baseUrl,
        balance: wallet.balance,
        created: new Date().toISOString(),
      }),
    });
    
    spinner.stop(`Wallet created with ${wallet.balance} sats`);
  } catch (error) {
    spinner.stop("Failed to create wallet", 1);
    throw error;
  }
  
  prompts.outro("Done");
  return;
}

1.4 Add Routstr to ModelsDev Provider

File: /packages/opencode/src/provider/models.ts

Ensure Routstr appears in provider listings by updating the local provider data or adding fallback provider info.

1.5 Test Integration

  • Test opencode auth login with Routstr option
  • Verify wallet creation with test Cashu tokens
  • Confirm API requests work with generated API keys

Expected Deliverables

  • Routstr appears in opencode auth list
  • Can create wallet with Cashu token
  • Basic API requests work
  • Error handling for invalid tokens

Stage 2: Internal Cashu Wallet (gonuts Integration)

Goals

  • Integrate gonuts library for internal wallet management
  • Store and manage Cashu tokens internally
  • Automatic token provisioning for API requests
  • Better balance tracking and management

Implementation Steps

2.1 Add gonuts Dependency

File: /packages/tui/go.mod

require (
    github.com/elnosh/gonuts v0.x.x  // Check latest version
)

2.2 Create Wallet Manager

File: /packages/tui/internal/wallet/cashu.go

package wallet

import (
    "context"
    "fmt"
    "path/filepath"
    
    "github.com/elnosh/gonuts/wallet"
    "github.com/sst/opencode/internal/global"
)

type CashuWallet struct {
    wallet *wallet.Wallet
    config *wallet.Config
}

func NewCashuWallet() (*CashuWallet, error) {
    configDir := Global.Path.data
    dbPath := filepath.Join(configDir, "cashu.db")
    
    config := &wallet.Config{
        WalletDataPath: dbPath,
        CurrentMintURL: "https://mint.minibits.cash/Bitcoin",
    }
    
    w, err := wallet.LoadWallet(config)
    if err != nil {
        // Create new wallet if doesn't exist
        w, err = wallet.CreateWallet(config)
        if err != nil {
            return nil, fmt.Errorf("failed to create wallet: %w", err)
        }
    }
    
    return &CashuWallet{
        wallet: w,
        config: config,
    }, nil
}

func (cw *CashuWallet) Balance(ctx context.Context) (uint64, error) {
    return cw.wallet.GetBalance()
}

func (cw *CashuWallet) ReceiveToken(ctx context.Context, token string) (uint64, error) {
    return cw.wallet.ReceiveTokens(token)
}

func (cw *CashuWallet) CreateToken(ctx context.Context, amount uint64) (string, error) {
    return cw.wallet.CreateToken(amount)
}

func (cw *CashuWallet) SendToken(ctx context.Context, amount uint64) (string, error) {
    return cw.wallet.SendTokens(amount)
}

2.3 Create Routstr Provider with Wallet Integration

File: /packages/tui/internal/wallet/routstr.go

package wallet

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
)

type RoutstrProvider struct {
    baseURL    string
    wallet     *CashuWallet
    currentKey string
}

func NewRoutstrProvider(baseURL string, wallet *CashuWallet) *RoutstrProvider {
    return &RoutstrProvider{
        baseURL: baseURL,
        wallet:  wallet,
    }
}

func (rp *RoutstrProvider) EnsureWallet(ctx context.Context) error {
    if rp.currentKey != "" {
        // Check if current key is still valid
        balance, err := rp.checkBalance(ctx)
        if err == nil && balance > 0 {
            return nil // Current key is good
        }
    }
    
    // Create new wallet from current balance
    balance, err := rp.wallet.Balance(ctx)
    if err != nil {
        return fmt.Errorf("failed to check wallet balance: %w", err)
    }
    
    if balance == 0 {
        return fmt.Errorf("wallet is empty, please top up")
    }
    
    // Create token for all balance
    token, err := rp.wallet.CreateToken(ctx, balance)
    if err != nil {
        return fmt.Errorf("failed to create token: %w", err)
    }
    
    // Create Routstr wallet
    apiKey, err := rp.createWallet(ctx, token)
    if err != nil {
        return fmt.Errorf("failed to create Routstr wallet: %w", err)
    }
    
    rp.currentKey = apiKey
    return nil
}

func (rp *RoutstrProvider) createWallet(ctx context.Context, cashuToken string) (string, error) {
    reqBody := map[string]string{
        "cashu_token": cashuToken,
    }
    
    jsonBody, _ := json.Marshal(reqBody)
    
    resp, err := http.Post(rp.baseURL+"/v1/wallet/create", "application/json", bytes.NewBuffer(jsonBody))
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    
    var result struct {
        APIKey  string `json:"api_key"`
        Balance int64  `json:"balance"`
    }
    
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return "", err
    }
    
    return result.APIKey, nil
}

func (rp *RoutstrProvider) checkBalance(ctx context.Context) (int64, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", rp.baseURL+"/v1/wallet/balance", nil)
    req.Header.Set("Authorization", "Bearer "+rp.currentKey)
    
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return 0, err
    }
    defer resp.Body.Close()
    
    var result struct {
        Balance int64 `json:"balance"`
    }
    
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return 0, err
    }
    
    return result.Balance, nil
}

func (rp *RoutstrProvider) GetAPIKey(ctx context.Context) (string, error) {
    if err := rp.EnsureWallet(ctx); err != nil {
        return "", err
    }
    return rp.currentKey, nil
}

2.4 Update Auth System for Wallet Storage

File: /packages/opencode/src/auth/routstr.ts

export interface RoutstrWalletConfig {
  baseUrl: string;
  mintUrl?: string;
  balance: number;
  lastUpdated: string;
}

export async function initializeWallet(cashuToken: string, config: RoutstrWalletConfig): Promise<void> {
  // Call TUI wallet initialization endpoint
  const response = await fetch('http://localhost:8000/wallet/initialize', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      cashu_token: cashuToken,
      base_url: config.baseUrl,
      mint_url: config.mintUrl,
    }),
  });
  
  if (!response.ok) {
    throw new Error(`Failed to initialize wallet: ${response.status}`);
  }
}

2.5 Add Wallet CLI Commands

File: /packages/opencode/src/cli/cmd/wallet.ts

export const WalletCommand = cmd({
  command: "wallet",
  describe: "manage cashu wallet",
  builder: (yargs) =>
    yargs
      .command(WalletBalanceCommand)
      .command(WalletTopupCommand)
      .command(WalletWithdrawCommand)
      .demandCommand(),
  async handler() {},
})

export const WalletBalanceCommand = cmd({
  command: "balance",
  describe: "show wallet balance",
  async handler() {
    // Call TUI wallet balance endpoint
    const response = await fetch('http://localhost:8000/wallet/balance');
    const data = await response.json();
    console.log(`Balance: ${data.balance} sats`);
  },
})

export const WalletTopupCommand = cmd({
  command: "topup <token>",
  describe: "add cashu token to wallet",
  builder: (yargs) =>
    yargs.positional("token", {
      describe: "cashu token to add",
      type: "string",
    }),
  async handler(args) {
    const response = await fetch('http://localhost:8000/wallet/topup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ cashu_token: args.token }),
    });
    
    const data = await response.json();
    console.log(`Added ${data.amount} sats. New balance: ${data.balance} sats`);
  },
})

Expected Deliverables

  • Internal Cashu wallet using gonuts
  • Automatic token management
  • CLI wallet commands (opencode wallet balance, topup, withdraw)
  • Seamless API key provisioning from wallet

Stage 3: Full UX Integration (Polish & Status Bar)

Goals

  • Display wallet balance in status bar
  • QR code generation for Lightning invoices
  • Automatic top-up prompts
  • Refund handling
  • Complete user experience

Implementation Steps

3.1 Add Wallet Balance to Status Bar

File: /packages/tui/internal/components/status/status.go

Add wallet balance field to statusComponent:

type statusComponent struct {
    app           *app.App
    width         int
    cwd           string
    branch        string
    walletBalance int64          // Add this
    watcher       *fsnotify.Watcher
    done          chan struct{}
    lastUpdate    time.Time
}

Update View() method to include balance:

func (m *statusComponent) View() string {
    // ... existing code ...
    
    // Add wallet balance display
    var balanceDisplay string
    if m.walletBalance > 0 {
        balanceDisplay = fmt.Sprintf("⚡%d sats", m.walletBalance)
    }
    
    // Update layout to include balance
    leftContent := logo + cwd
    if balanceDisplay != "" {
        leftContent += faintStyle.Render(" | ") + 
                      styles.NewStyle().Foreground(t.AccentColor()).Render(balanceDisplay)
    }
    
    // ... rest of layout code ...
}

Add balance update message handling:

type WalletBalanceUpdatedMsg struct {
    Balance int64
}

func (m *statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case WalletBalanceUpdatedMsg:
        m.walletBalance = msg.Balance
        return m, nil
    // ... existing cases ...
    }
    return m, nil
}

3.2 Create Wallet Balance Monitor

File: /packages/tui/internal/wallet/monitor.go

package wallet

import (
    "context"
    "time"
    
    tea "github.com/charmbracelet/bubbletea/v2"
)

type BalanceMonitor struct {
    provider *RoutstrProvider
    program  *tea.Program
    interval time.Duration
    done     chan struct{}
}

func NewBalanceMonitor(provider *RoutstrProvider, program *tea.Program) *BalanceMonitor {
    return &BalanceMonitor{
        provider: provider,
        program:  program,
        interval: 30 * time.Second,
        done:     make(chan struct{}),
    }
}

func (bm *BalanceMonitor) Start(ctx context.Context) {
    ticker := time.NewTicker(bm.interval)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            balance, err := bm.provider.checkBalance(ctx)
            if err == nil {
                bm.program.Send(WalletBalanceUpdatedMsg{Balance: balance})
                
                // Check for low balance
                if balance < 1000 { // Less than 1000 sats
                    bm.program.Send(LowBalanceWarningMsg{Balance: balance})
                }
            }
        case <-bm.done:
            return
        case <-ctx.Done():
            return
        }
    }
}

func (bm *BalanceMonitor) Stop() {
    close(bm.done)
}

3.3 Add Lightning Invoice Generation

File: /packages/tui/internal/wallet/lightning.go

package wallet

import (
    "fmt"
    "net/http"
    "encoding/json"
    "bytes"
)

type InvoiceRequest struct {
    Amount      int64  `json:"amount"`
    Description string `json:"description"`
}

type InvoiceResponse struct {
    PaymentRequest string `json:"payment_request"`
    PaymentHash    string `json:"payment_hash"`
    CheckingID     string `json:"checking_id"`
}

func (cw *CashuWallet) CreateInvoice(amount int64, description string) (*InvoiceResponse, error) {
    mintURL := cw.config.CurrentMintURL
    
    reqBody := InvoiceRequest{
        Amount:      amount,
        Description: description,
    }
    
    jsonBody, _ := json.Marshal(reqBody)
    
    resp, err := http.Post(mintURL+"/v1/mint/quote/bolt11", "application/json", bytes.NewBuffer(jsonBody))
    if err != nil {
        return nil, fmt.Errorf("failed to create invoice: %w", err)
    }
    defer resp.Body.Close()
    
    var result InvoiceResponse
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("failed to parse invoice response: %w", err)
    }
    
    return &result, nil
}

3.4 Create Low Balance Dialog

File: /packages/tui/internal/components/dialog/topup.go

package dialog

import (
    "fmt"
    
    tea "github.com/charmbracelet/bubbletea/v2"
    "github.com/charmbracelet/lipgloss/v2"
    "github.com/mdp/qrterminal/v3"
)

type TopupDialog struct {
    invoice       string
    qrCode        string
    amount        int64
    width         int
    height        int
}

func NewTopupDialog(invoice string, amount int64) *TopupDialog {
    var qrCode string
    qrterminal.GenerateWithConfig(invoice, qrterminal.Config{
        Level:     qrterminal.M,
        Writer:    &qrCodeBuffer{},
        BlackChar: qrterminal.BLACK,
        WhiteChar: qrterminal.WHITE,
    })
    
    return &TopupDialog{
        invoice: invoice,
        qrCode:  qrCode,
        amount:  amount,
    }
}

func (d *TopupDialog) View() string {
    title := fmt.Sprintf("Top up wallet with %d sats", d.amount)
    
    content := lipgloss.JoinVertical(
        lipgloss.Center,
        title,
        "",
        "Scan QR code with your Lightning wallet:",
        "",
        d.qrCode,
        "",
        "Or copy this invoice:",
        d.invoice,
        "",
        "Press 'q' to close",
    )
    
    return lipgloss.Place(d.width, d.height, lipgloss.Center, lipgloss.Center, content)
}

3.5 Add Automatic Top-up Flow

File: /packages/tui/internal/app/app.go

Add low balance handling:

func (a *App) HandleLowBalance(balance int64) tea.Cmd {
    return func() tea.Msg {
        // Create Lightning invoice for top-up
        wallet, err := wallet.NewCashuWallet()
        if err != nil {
            return ErrorMsg{Error: err}
        }
        
        suggestedAmount := int64(10000) // 10k sats
        if balance < 1000 {
            suggestedAmount = 5000 // 5k sats for very low balance
        }
        
        invoice, err := wallet.CreateInvoice(suggestedAmount, "OpenCode wallet top-up")
        if err != nil {
            return ErrorMsg{Error: err}
        }
        
        return ShowTopupDialogMsg{
            Invoice: invoice.PaymentRequest,
            Amount:  suggestedAmount,
        }
    }
}

3.6 Add Refund Handling

File: /packages/tui/internal/wallet/refund.go

package wallet

func (rp *RoutstrProvider) RequestRefund(ctx context.Context) (string, error) {
    req, _ := http.NewRequestWithContext(ctx, "POST", rp.baseURL+"/v1/wallet/refund", nil)
    req.Header.Set("Authorization", "Bearer "+rp.currentKey)
    
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    
    var result struct {
        Token string `json:"token"`
    }
    
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return "", err
    }
    
    // Receive the refund token into our wallet
    if result.Token != "" {
        _, err = rp.wallet.ReceiveToken(ctx, result.Token)
        if err != nil {
            return "", fmt.Errorf("failed to receive refund: %w", err)
        }
    }
    
    return result.Token, nil
}

3.7 Enhanced CLI Commands

File: /packages/opencode/src/cli/cmd/wallet.ts

Add QR code support and enhanced UX:

export const WalletTopupCommand = cmd({
  command: "topup [amount]",
  describe: "top up wallet via Lightning",
  builder: (yargs) =>
    yargs.positional("amount", {
      describe: "amount in sats",
      type: "number",
      default: 10000,
    }),
  async handler(args) {
    const response = await fetch('http://localhost:8000/wallet/invoice', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ amount: args.amount }),
    });
    
    const data = await response.json();
    
    // Display QR code in terminal
    console.log(`⚡ Lightning Invoice for ${args.amount} sats`);
    console.log();
    displayQRCode(data.payment_request);
    console.log();
    console.log("Invoice:", data.payment_request);
    console.log();
    console.log("Waiting for payment...");
    
    // Poll for payment
    await pollForPayment(data.checking_id);
    console.log("✅ Payment received!");
  },
})

Expected Deliverables

  • Wallet balance displayed in status bar
  • Automatic low balance detection and top-up prompts
  • QR code generation for Lightning invoices
  • Seamless refund handling
  • Complete user experience with visual feedback

Technical Architecture Decisions

Database Schema

File: /packages/tui/internal/storage/schema.sql

CREATE TABLE IF NOT EXISTS wallet_state (
    id INTEGER PRIMARY KEY,
    balance INTEGER NOT NULL DEFAULT 0,
    mint_url TEXT NOT NULL,
    seed TEXT NOT NULL, -- Encrypted
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS routstr_sessions (
    id TEXT PRIMARY KEY,
    api_key TEXT NOT NULL,
    base_url TEXT NOT NULL,
    balance INTEGER NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_used DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS transactions (
    id TEXT PRIMARY KEY,
    type TEXT NOT NULL, -- 'topup', 'spend', 'refund'
    amount INTEGER NOT NULL,
    description TEXT,
    cashu_token TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Configuration Updates

File: /packages/opencode/opencode.json

{
  "providers": {
    "routstr": {
      "name": "Routstr",
      "description": "Bitcoin Lightning AI payments",
      "baseUrl": "https://api.routstr.com",
      "mintUrl": "https://mint.minibits.cash/Bitcoin",
      "models": ["anthropic/claude-sonnet-4", "meta-llama/llama-3.2-1b-instruct"],
      "defaultModel": "anthropic/claude-sonnet-4"
    }
  }
}

Error Handling Strategy

  1. Insufficient Balance:

    • Show current balance and required amount
    • Offer immediate top-up with Lightning invoice
    • Allow continuing with different model/provider
  2. Token Validation Failures:

    • Clear guidance on token format
    • Link to supported wallet applications
    • Fallback to manual entry
  3. Network Connectivity:

    • Offline mode detection
    • Graceful degradation
    • Retry mechanisms with exponential backoff
  4. Wallet Corruption:

    • Backup and recovery mechanisms
    • Safe wallet reset options
    • Data export capabilities

Security Considerations

Token Storage

  • Encrypt Cashu tokens at rest using system keyring
  • Never log or expose tokens in plaintext
  • Implement secure token rotation

Network Security

  • Always use HTTPS for Routstr communication
  • Validate SSL certificates
  • Implement request signing for sensitive operations

Wallet Security

  • Use BIP39 mnemonic seed generation
  • Encrypt wallet database with user-derived key
  • Implement wallet backup and recovery

Privacy

  • No personal information required
  • Anonymous payment flows
  • Optional Tor support for enhanced privacy

Testing Strategy

Unit Tests

  • Cashu wallet operations
  • Routstr API integration
  • Balance calculations and conversions
  • Token validation and formatting

Integration Tests

  • End-to-end auth flow
  • API request/response cycles
  • Balance updates and monitoring
  • Refund processing

Manual Testing Scenarios

  1. Fresh installation: No existing wallet, first-time setup
  2. Existing wallet: Resuming with existing balance
  3. Low balance: Handling insufficient funds scenarios
  4. Network issues: Offline/online transitions
  5. Multiple sessions: Concurrent usage patterns

Deployment Considerations

Backwards Compatibility

  • Maintain existing auth.json format
  • Graceful fallback for non-Routstr providers
  • Optional Routstr features (don't break existing workflows)

Documentation Updates

  • Update CLI help text
  • Add Routstr provider documentation
  • Create wallet management guides
  • Update troubleshooting sections

Monitoring & Analytics

  • Track wallet creation success rates
  • Monitor balance usage patterns
  • Log payment failures and retries
  • Measure user engagement with Bitcoin payments

Success Metrics

Stage 1 Success Criteria

  • Routstr appears in provider list
  • Can authenticate with Cashu token
  • Successfully make API requests
  • Basic error handling works

Stage 2 Success Criteria

  • Internal wallet stores and manages tokens
  • Automatic token provisioning works
  • CLI wallet commands functional
  • Balance tracking accurate

Stage 3 Success Criteria

  • Status bar shows wallet balance
  • Lightning invoice generation works
  • QR codes display properly
  • Low balance prompts appear
  • Refund mechanism functional
  • End-to-end user experience smooth

Conclusion

This integration plan provides a comprehensive approach to adding Bitcoin Lightning payments to OpenCode through Routstr. The three-stage approach ensures incremental delivery of value while maintaining system stability and user experience quality.

The recommended Persistent Wallet Model (Approach A) provides the best balance of implementation complexity and user experience, leveraging existing OpenCode patterns while abstracting the underlying payment complexity.

Key benefits of this integration:

  • Anonymous payments: No personal information or credit cards required
  • Instant settlement: Immediate payment processing via Lightning Network
  • Micropayment friendly: Pay only for what you use
  • Self-sovereign: User controls their own funds and keys
  • Privacy-focused: Bitcoin-native payment flows

The plan addresses the complex challenges of token lifecycle management, balance tracking, and user experience while maintaining OpenCode's existing architecture and usability standards.