Extension: Cash Provider (ATM liquidity funding) #15

Open
opened 2026-04-02 18:38:41 +00:00 by padreug · 1 comment
Owner

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).

  • 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)
## 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 ```typescript 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)
Author
Owner

Enhancement: Per-provider commission rates

Each cash provider should be able to set their own desired commission rate when funding a machine. For example:

  • Provider A funds $5,000 at 10% commission
  • Provider B funds $3,000 at 5% commission

When a customer does a cash-out, each provider's share of the BTC settlement has their own commission deducted — not a flat rate for all providers.

Data model change

Add commission_rate to CashFunding:

CashFunding {
  // ... existing fields ...
  commission_rate: number | null   // provider's desired rate (e.g. 0.10 = 10%)
                                   // null = use machine default
}

Settlement calculation

Customer cash-out: $100 dispensed, 250,000 sats received

Provider A: $3,000 remaining (60%), commission 10%
  → share: 150,000 sats
  → commission: 15,000 sats
  → provider receives: 135,000 sats

Provider B: $2,000 remaining (40%), commission 5%
  → share: 100,000 sats
  → commission: 5,000 sats
  → provider receives: 95,000 sats

Total commission: 20,000 sats (split per LP #13 weights)

UX

Provider specifies their rate in the fundMachine call:

satmachine.fundMachine({
  machine_id: "atm-antigua-01",
  amount: 5000,
  currency: "USD",
  commission_rate: 0.10   // 10%, or omit for machine default
})

This creates a competitive dynamic — providers who accept lower commission are more attractive to operators, while providers who want higher margins can set their own price. The operator can also set a minimum/maximum commission range per machine to keep rates reasonable.

## Enhancement: Per-provider commission rates Each cash provider should be able to set their own desired commission rate when funding a machine. For example: - Provider A funds $5,000 at 10% commission - Provider B funds $3,000 at 5% commission When a customer does a cash-out, each provider's share of the BTC settlement has their own commission deducted — not a flat rate for all providers. ### Data model change Add `commission_rate` to `CashFunding`: ```typescript CashFunding { // ... existing fields ... commission_rate: number | null // provider's desired rate (e.g. 0.10 = 10%) // null = use machine default } ``` ### Settlement calculation ``` Customer cash-out: $100 dispensed, 250,000 sats received Provider A: $3,000 remaining (60%), commission 10% → share: 150,000 sats → commission: 15,000 sats → provider receives: 135,000 sats Provider B: $2,000 remaining (40%), commission 5% → share: 100,000 sats → commission: 5,000 sats → provider receives: 95,000 sats Total commission: 20,000 sats (split per LP #13 weights) ``` ### UX Provider specifies their rate in the `fundMachine` call: ``` satmachine.fundMachine({ machine_id: "atm-antigua-01", amount: 5000, currency: "USD", commission_rate: 0.10 // 10%, or omit for machine default }) ``` This creates a competitive dynamic — providers who accept lower commission are more attractive to operators, while providers who want higher margins can set their own price. The operator can also set a minimum/maximum commission range per machine to keep rates reasonable.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: aiolabs/lightning-pub#15
No description provided.