padreug opened issue aiolabs/lightning-pub#15

padreug
2026-04-02T18:38:41Z

15#Extension: Cash Provider (ATM liquidity funding)#

Summary

Implement a Lightning.Pub extension that tracks cash liquidity providers for ATMs. Providers physically fund ATM cartridges with fiat and receive BTC as customers do cash-out transactions. The machine tracks how much it owes each provider and settles in BTC automatically.

This replaces the LNbits satmachineadmin DCA system with a Nostr-native approach — no SSH tunnels, no database polling, no separate admin/client extensions.

How It Works

Provider funds the ATM

1. Provider hands cash to operator (or loads directly)
2. Provider's wallet calls: satmachine.fundMachine
   → { machine_id, amount: 5000, currency: "USD" }
   → Encrypted via NIP-44 (kind 21000 RPC)
3. Extension records funding with status: "in_flight"
4. Operator confirms cash is in cartridge: satmachine.confirmDeposit
   → Status changes to "deposited"

Customer does cash-out (sells BTC for cash)

1. Customer sends BTC to ATM via Lightning
2. ATM dispenses cash from cartridge
3. Extension calculates provider settlements:
   a. Commission extracted per dynamic rules (lamassu-next #6)
   b. Commission split per configured weights (ties to LP #13)
   c. Remaining BTC distributed to providers proportionally by remaining balance
4. Providers receive BTC via payInvoice() / keysend
5. Provider fiat balances reduced by amount dispensed
6. Providers notified via NIP-17 encrypted DM

Multiple providers

Proportional distribution when cash-out happens. If provider A has $3,000 remaining and provider B has $2,000 remaining, a $100 cash-out settles 60% to A and 40% to B.

Data Model

CashFunding {
  id: string
  machine_id: string            // which ATM
  provider_pubkey: string       // npub of cash provider
  amount: number                // fiat amount funded
  currency: string              // "USD", "GTQ"
  status: 'in_flight' | 'deposited'
  remaining: number             // fiat still owed to provider
  created_at: number
  deposited_at: number | null
}

ProviderSettlement {
  id: string
  funding_id: string            // links to CashFunding
  provider_pubkey: string
  amount_sats: number           // BTC paid to provider
  amount_fiat: number           // fiat value settled
  commission_sats: number       // commission deducted
  payment_hash: string
  cashout_event_id: string      // which cash-out triggered this
  created_at: number
}

MachineConfig {
  id: string
  machine_id: string
  commission_percentage: number
  commission_split: CommissionSplit[]  // ties to split payments (LP #13)
  currency: string
  created_at: number
  updated_at: number
}

CommissionSplit {
  recipient_pubkey: string
  weight: number                // NIP-57 style relative weights
  alias: string | null
}

RPC Methods

Provider methods (auth: user pubkey)

satmachine.fundMachine          // Declare cash injection
satmachine.getProviderBalance   // Query what you're owed (all machines)
satmachine.getProviderHistory   // Settlement history
satmachine.getProviderAnalytics // Cost basis, accumulation stats

Operator methods (auth: admin)

satmachine.confirmDeposit       // Confirm cash is in cartridge
satmachine.listFundings         // View all active fundings
satmachine.listMachines         // View configured machines
satmachine.configureMachine     // Set commission, currency, split config
satmachine.setCommissionSplit   // Configure split weights (LP #13)
satmachine.listSettlements      // Audit trail of all settlements
satmachine.exportHistory        // Export for accounting

ExtensionContext API Usage

API | Use
registerMethod() | All RPC methods above
onPaymentReceived() | Detect cash-out events (customer BTC arriving)
payInvoice() | Settle BTC to providers
sendEncryptedDM() | Notify providers of settlements (NIP-17)
getDatabase() | Store fundings, settlements, config
onNostrEvent() | Subscribe to ATM transaction events (kind 30079)
log() | Audit logging

Provider Dashboard (via RPC)

Providers query their balance from any Nostr client. Responses are NIP-44 encrypted — only the provider sees their data.

satmachine.getProviderBalance response:
{
  machines: [
    {
      machine_id: "atm-antigua-01",
      fundings: [
        { id: "f1", amount: 5000, currency: "USD", remaining: 3200, status: "deposited" },
        { id: "f2", amount: 2000, currency: "USD", remaining: 2000, status: "in_flight" }
      ],
      total_owed_fiat: 5200,
      total_settled_sats: 450000,
      total_commission_sats: 13500
    }
  ],
  total_owed_fiat: 5200,
  total_settled_sats: 450000,
  average_cost_basis: 250.0  // sats per USD
}

Commission Integration

Commission calculation ties to two other systems:

lamassu-next #6 (Dynamic Commission): Determines the commission amount per transaction (tiered rates, mempool fee adjustment)

LP #13 (Split Payments): Determines the commission distribution (operator gets X%, provider gets Y%, using NIP-57 weight system)

The cash provider extension owns the settlement logic: after commission is extracted and split, the remaining BTC goes to providers proportionally.

Nostr Transport

All communication uses existing Lightning.Pub extension RPC (kind 21000):

Provider → ATM: encrypted method calls via Nostr relay

ATM → Provider: encrypted responses + NIP-17 DMs for settlement notifications

No new event kinds needed

No HTTP endpoints needed (pure Nostr)

Exception: the extension subscribes to kind 30079 (transaction records) from the ATM via onNostrEvent() to detect when cash-outs happen.

Key Differences from LNbits satmachineadmin

Aspect | LNbits (old) | Lightning.Pub (new)
Data source | SSH tunnel to Lamassu PostgreSQL | Nostr events (kind 30079)
Trigger | Hourly polling cron | Real-time event-driven
Provider identity | LNbits wallet ID | Nostr pubkey
Extensions | Two (admin + client) | One (role by pubkey)
Cash tracking | Deposits confirmed in web UI | fundMachine + confirmDeposit RPC
Settlement | LNbits internal payment | payInvoice() via ExtensionContext
Provider dashboard | Web UI (Vue/Quasar) | Any Nostr client via RPC queries
Commission split | Not integrated | Ties to split payments extension (LP #13)
Comms privacy | HTTP + API keys | NIP-44 encrypted end-to-end

Complexity

MEDIUM — ~2000-3000 lines TypeScript. The commission math and proportional distribution are well-defined. The main complexity is the settlement state machine (funding → deposit → partial settlements → fully settled).

Related Issues

LP #11: Original satmachine issue (DCA-focused, superseded by this approach)

LP #13: Split Payments extension (commission distribution)

lamassu-next #6: Dynamic Commission System (commission calculation)

lamassu-next #4: Optional npub identification for cash-in (same NIP-17 pattern)

lamassu-next #1: Public Cash Availability Display (provider funding affects displayed availability)