15 KiB
Payment Methods
Routstr supports multiple payment methods for funding temporary API key balances. This document describes the payment method architecture and how to add new payment methods.
Overview
The payment method system is built on an abstract base class that defines a consistent interface for:
- Detecting valid payment credentials
- Receiving and crediting payments
- Refunding balances back to the original payment source
- Extracting refund-related metadata
Architecture
Core Components
AbstractPaymentMethod: Abstract base class that all payment methods must implementPaymentCredentials: Data class containing parsed credential informationPaymentResult: Data class containing payment processing results- Payment Method Registry: Automatic detection and routing to appropriate payment handlers
Payment Flow
User provides credential (token/invoice/tx)
↓
get_payment_method() detects appropriate handler
↓
parse_credential() validates format
↓
receive_payment() processes and credits balance
↓
refund_payment() returns funds when requested
Implemented Payment Methods
1. Cashu eCash (Fully Implemented)
Status: ✅ Fully functional
Credentials: Starts with cashu
Implementation: CashuPaymentMethod
Features:
- Token redemption from multiple mints
- Automatic mint detection and swapping
- Lightning address refunds via LNURL
- Cashu token refunds
- Multi-currency support (sats, msats)
Refund Options:
- Lightning address (via LNURL)
- New Cashu token
2. Bitcoin Lightning Network (Pseudo-Implementation)
Status: 🚧 Interface defined, implementation pending
Credentials: Starts with ln or lnbc (Lightning invoices)
Implementation: LightningPaymentMethod
To Fully Implement:
-
Add Lightning Library Dependency
# In pyproject.toml dependencies = [ "lnbits-client>=0.1.0", # or lnd-grpc, c-lightning-python ] -
Configure Lightning Node
# In core/settings.py lightning_host: str = Field(default="localhost:10009") lightning_macaroon: str = Field(default="") lightning_cert_path: str | None = None -
Update ApiKey Model
# Add to ApiKey in core/db.py lightning_payment_hash: str | None = None lightning_preimage: str | None = None -
Implement Invoice Verification
async def receive_payment(self, credential: str, key: ApiKey, session: AsyncSession): node = LightningNode(host=settings.lightning_host) invoice = await node.decode_invoice(credential) # Wait for payment payment = await node.lookup_invoice(invoice.payment_hash) if not payment.settled: raise ValueError("Invoice not yet paid") # Credit balance amount_msats = payment.amount_msat # ... (atomic balance update) return PaymentResult( amount_msats=amount_msats, currency="btc", payment_method="lightning", transaction_id=invoice.payment_hash, ) -
Implement Refunds
- Fetch LNURL data from lightning address
- Request invoice from LNURL service
- Pay invoice using lightning node
- Return payment hash and preimage
-
Add Webhook/Polling
- Monitor invoice payments in background
- Update balance automatically when paid
3. USDT (Tether) Stablecoin (Pseudo-Implementation)
Status: 🚧 Interface defined, implementation pending
Credentials: Starts with 0x (Ethereum tx), usdt:, or tether:
Implementation: USDTetherPaymentMethod
To Fully Implement:
-
Choose Blockchain Network
- Ethereum (ERC-20)
- Tron (TRC-20)
- Liquid Network
- Lightning Network (Taproot Assets/RGB)
-
Add Blockchain Library
# For Ethereum/ERC-20 dependencies = [ "web3>=6.0.0", ] -
Configure Blockchain Node/API
# In core/settings.py ethereum_rpc_url: str = Field(default="https://mainnet.infura.io/v3/...") usdt_contract_address: str = Field(default="0xdAC17F958D2ee523a2206206994597C13D831ec7") usdt_receiving_address: str = Field(default="") usdt_hot_wallet_address: str = Field(default="") usdt_private_key: str = Field(default="") usdt_required_confirmations: int = Field(default=3) -
Update ApiKey Model
# Add to ApiKey in core/db.py usdt_chain: str | None = None # "ethereum", "tron", etc. usdt_tx_hash: str | None = None usdt_sender_address: str | None = None -
Implement Transaction Verification
async def receive_payment(self, credential: str, key: ApiKey, session: AsyncSession): w3 = Web3(Web3.HTTPProvider(settings.ethereum_rpc_url)) tx_receipt = w3.eth.get_transaction_receipt(credential) # Verify USDT transfer # Parse Transfer event from logs amount_usdt = self._decode_transfer_amount(tx_receipt) # Wait for confirmations current_block = w3.eth.block_number confirmations = current_block - tx_receipt.blockNumber if confirmations < settings.usdt_required_confirmations: raise ValueError(f"Insufficient confirmations: {confirmations}") # Convert USDT to msats using exchange rate rate = await get_usdt_to_sats_rate() amount_sats = int(amount_usdt * rate) amount_msats = amount_sats * 1000 # Credit balance atomically # ... (atomic balance update) return PaymentResult( amount_msats=amount_msats, currency="usdt", payment_method="usdt", transaction_id=credential, ) -
Implement Refunds
- Estimate gas fees
- Build USDT transfer transaction
- Sign with hot wallet
- Broadcast to network
- Handle gas payment
-
Add Exchange Rate Service
async def get_usdt_to_sats_rate() -> float: # Use price API (CoinGecko, Binance, etc.) # Return rate: 1 USDT = X sats pass -
Monitor Blockchain Events
- Set up event listener for incoming USDT
- Auto-credit balances on confirmation
4. On-Chain Bitcoin (Pseudo-Implementation)
Status: 🚧 Interface defined, implementation pending
Credentials: 64-character hex (txid), bitcoin:, or btc:
Implementation: OnChainBitcoinPaymentMethod
To Fully Implement:
-
Add Bitcoin Library
dependencies = [ "bitcoinlib>=0.6.0", # or "bitcoin-python>=0.7.0" ] -
Configure Bitcoin Node
# In core/settings.py bitcoin_rpc_url: str = Field(default="http://user:pass@localhost:8332") btc_receiving_address: str = Field(default="") btc_hot_wallet_address: str = Field(default="") btc_change_address: str = Field(default="") btc_required_confirmations: int = Field(default=3) -
Implement HD Wallet
- Generate unique deposit address per ApiKey
- Store derivation path in database
-
Update ApiKey Model
# Add to ApiKey in core/db.py btc_deposit_address: str | None = None btc_deposit_txid: str | None = None btc_address_index: int | None = None -
Implement Transaction Monitoring
- Poll for incoming transactions
- Verify confirmations
- Prevent double-processing
-
Implement Refunds
- UTXO management
- Coin selection algorithm
- Fee estimation
- Transaction building and signing
- Broadcasting
Creating a Custom Payment Method
Step 1: Implement the Abstract Class
from routstr.payment.methods import AbstractPaymentMethod, PaymentCredentials, PaymentResult
from routstr.core.db import ApiKey, AsyncSession
class MyCustomPaymentMethod(AbstractPaymentMethod):
def can_handle(self, credential: str) -> bool:
"""Return True if this method can process the credential."""
return credential.startswith("mycustom:")
async def parse_credential(self, credential: str) -> PaymentCredentials:
"""Parse and validate the credential."""
# Parse credential format
# Extract metadata
return PaymentCredentials(
raw_credential=credential,
payment_type="mycustom",
metadata={"amount": 1000, "id": "..."},
)
async def receive_payment(
self, credential: str, key: ApiKey, session: AsyncSession
) -> PaymentResult:
"""Process the payment and credit the balance."""
# 1. Verify payment is valid
# 2. Calculate amount in msats
# 3. Credit balance atomically
from sqlmodel import col, update
amount_msats = 1000000 # Your calculation
stmt = (
update(ApiKey)
.where(col(ApiKey.hashed_key) == key.hashed_key)
.values(balance=ApiKey.balance + amount_msats)
)
await session.exec(stmt)
await session.commit()
await session.refresh(key)
return PaymentResult(
amount_msats=amount_msats,
currency="custom",
payment_method="mycustom",
transaction_id="...",
)
async def refund_payment(
self, key: ApiKey, amount_msats: int | None = None
) -> dict[str, str]:
"""Refund the balance."""
# 1. Calculate refund amount
# 2. Send refund via your payment system
# 3. Return details
return {
"transaction_id": "...",
"recipient": key.refund_address or "",
"amount_msats": str(amount_msats or key.balance),
"method": "mycustom",
}
def get_refund_metadata(self, key: ApiKey) -> dict[str, str]:
"""Extract refund metadata from ApiKey."""
return {
"refund_address": key.refund_address or "",
"payment_method": "mycustom",
}
Step 2: Register the Payment Method
from routstr.payment.methods import register_payment_method
# In your initialization code (e.g., core/main.py startup)
register_payment_method(MyCustomPaymentMethod())
Step 3: Configure Settings (if needed)
# In core/settings.py
class Settings(BaseSettings):
# ... existing settings ...
# Add your payment method settings
mycustom_api_key: str = Field(default="")
mycustom_endpoint: str = Field(default="https://api.example.com")
Payment Method Priority
Payment methods are checked in the order they were registered. The first method where can_handle() returns True will be used.
Default order:
- Cashu
- Lightning
- USDT
- On-chain Bitcoin
- Custom methods (in registration order)
Database Considerations
Current ApiKey Fields
The ApiKey model currently includes:
balance: Available balance in msatsreserved_balance: Reserved for pending requestsrefund_address: Address/LNURL for refundsrefund_currency: Currency for refunds (sat, msat, etc.)refund_mint_url: Cashu mint URL for refundskey_expiry_time: Expiry timestamp for auto-refundtotal_spent: Total msats spenttotal_requests: Total API requests made
Recommended Extensions
For multi-payment-method support, consider adding:
# In core/db.py ApiKey model
payment_method_type: str | None = Field(
default=None,
description="Payment method used (cashu, lightning, usdt, bitcoin)"
)
payment_metadata: str | None = Field(
default=None,
description="JSON-encoded payment method specific metadata"
)
This allows:
- Automatic payment method detection for refunds
- Storage of method-specific data without schema changes
- Better analytics and reporting
Testing
Unit Tests
Test each payment method in isolation:
from routstr.payment.methods import CashuPaymentMethod
async def test_cashu_can_handle():
method = CashuPaymentMethod()
assert method.can_handle("cashuA...")
assert not method.can_handle("lnbc...")
async def test_cashu_parse_credential():
method = CashuPaymentMethod()
creds = await method.parse_credential("cashuA...")
assert creds.payment_type == "cashu"
assert creds.metadata is not None
Integration Tests
Test end-to-end payment flows:
async def test_payment_flow():
# Create credential
credential = generate_test_credential()
# Validate and credit
key = await validate_bearer_key(credential, session)
assert key.balance > 0
# Topup
await topup_wallet_endpoint(credential, key, session)
# Refund
result = await refund_wallet_endpoint(key)
assert "token" in result or "recipient" in result
Security Considerations
- Credential Validation: Always validate credentials thoroughly before processing
- Idempotency: Prevent duplicate processing of the same payment
- Atomic Operations: Use atomic SQL updates to prevent race conditions
- Private Keys: Never log or expose private keys
- Rate Limiting: Implement rate limits on payment processing endpoints
- Confirmation Requirements: Wait for sufficient blockchain confirmations
- Amount Limits: Consider minimum and maximum payment amounts
- Refund Address Validation: Verify refund addresses belong to the original payer
API Endpoints
Create Wallet
GET /create?initial_balance_token=<credential>
Creates a new API key from a payment credential.
Topup
POST /topup
Content-Type: application/json
{
"cashu_token": "<credential>"
}
Adds funds to an existing API key.
Refund
POST /refund
Authorization: Bearer <credential>
Refunds the remaining balance.
Balance Info
GET /info
Authorization: Bearer sk-<key>
Returns current balance and reserved amount.
Future Enhancements
- Multi-Chain Support: Support USDT on multiple chains simultaneously
- Automatic Conversion: Convert between payment methods automatically
- Partial Refunds: Allow refunding specific amounts instead of full balance
- Payment History: Track all incoming/outgoing payments per key
- Scheduled Refunds: Auto-refund after expiry time
- Payment Webhooks: Notify external systems of payment events
- Fee Customization: Per-method fee configuration
- Payment Routing: Intelligent routing based on amount, speed, fees
Resources
Support
For questions or issues with payment methods:
- Check this documentation
- Review the source code in
routstr/payment/methods.py - Open an issue on GitHub
- Join our community chat