Purpose: Detect bank payment notifications, extract Cash Account from reference, match to covenant, auto-fund with BCH
Complexity: High - Android notification interception, regex parsing, payment matching, auto-funding
Status (September 8, 2026):
Phase 0 ✅ Implemented:
Phase 0 🔨 In progress (Bizum auto-funding — see seller-auto-funding workspace):
Phase 1+ 🔨 Future:
Important: This document’s comprehensive bank-notification parsing describes seller auto-funding. Per the seller-auto-funding decision (Sep 2026), the MVP on-ramp is cash-in-person first (same construct+fund core, manual trigger); Bizum auto-funding builds on it as the Phase 0 second milestone. Phase 0 currently implements Telegram parameter parsing (recipient-side).
Telegram is NOT the product’s coordination transport. It is:
[COVENANT_V25] blocks)The production coordination layer is Nostr (encrypted DMs, parameter transport). See nostr.md.
Phase 1+ broader transport: the parameter-sharing/notification flow should work across multiple messengers — WhatsApp, LINE, Signal, SMS, etc. This is a low-priority enhancement, but the design should not assume Telegram specifically. The pattern that makes this easy: parameters are always a structured text block ([COVENANT_V25] / [CASH_IN_PERSON]) that any transport can carry, and the NotificationListener parses by tag, not by app.
Implication for implementation: keep the “copy to clipboard → paste anywhere” pattern (transport-agnostic), and keep the parser tag-based. Don’t hardcode Telegram-specific behavior beyond the Phase 0 listener.
The Notification Bot has two responsibilities:
Phase 1+ approach: Auto-fund only if wallet has sufficient BCH (show notification if insufficient) Phase 1++ extension: Auto-buy from exchange if wallet balance low (see Optional Extensions section)
Implementation approach: Android Notification Listener Service (intercepts bank app notifications)
Why push notifications, not SMS? Banks are migrating from SMS to push notifications because:
Legacy SMS support: Some users (grandfathered accounts) still receive SMS for Bizum payments. This is increasingly rare as banks phase out SMS to reduce costs. The smsbridge_loop.py prototype demonstrated SMS parsing works, but Notification Listener Service is the primary approach for Phase 0.
The bot also maintains Nostr subscriptions to market price feeds for covenant monitoring:
asgaya:market:bch-eur (trade broadcasts from sellers)asgaya:oracle:asgaya (Kraken API bootstrap)See: Distributed Monitoring for blockchain-as-oracle architecture.
Cost: Zero (pure Nostr subscription, no HTTP polling). 150× reduction vs traditional HTTP API polling.
1. María creates covenant for Elena#142 (€100 BCH)
↓ [Covenant broadcast to bulletin board]
2. Isabel has active listing ("Sell €100-500 EUR")
↓ [Notification Bot running in background]
3. María sends Bizum to Isabel: +34612345678, ref "Elena#142"
↓ [Bizum app sends notification: "Received €100.50 from María"]
4. Isabel's Notification Bot intercepts notification
↓ [Parse: amount=€100.50, reference="Elena#142"]
5. Bot queries Electrum: "Find covenant for Elena#142"
↓ [Finds María's unfunded covenant]
6. Bot checks Isabel's wallet: Has 0.0234 BCH? (€107 at current rate)
↓ [Yes: Fund covenant. No: Notify Isabel to top up wallet]
7. Bot funds covenant with €107 BCH (if wallet has sufficient balance)
↓ [Broadcast funding transaction]
8. Elena gets notified (Electrum detects funded covenant)
↓ [Elena claims €100 BCH, €7 buffer returns to Isabel later]
All automatic for Isabel: She just receives fiat, BCH locked without touching phone.
What: Android API that intercepts app notifications
Setup:
AndroidManifest.xml:
<service android:name=".NotificationListener"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.notification.action.NOTIFICATION_LISTENER_SERVICE" />
</intent-filter>
</service>
Pseudocode:
class NotificationListener extends NotificationListenerService {
function onNotificationPosted(notification):
// Check if notification is from bank app
app_package = notification.packageName
if app_package not in SUPPORTED_BANK_APPS:
return // Ignore non-bank notifications
// Extract notification text
title = notification.extras.get("android.title")
text = notification.extras.get("android.text")
full_text = title + " " + text
// Parse payment details
payment = parsePaymentNotification(full_text, app_package)
if payment:
handlePaymentReceived(payment)
}
Supported bank apps (Phase 1+ - Planned):
com.bbva.bbvacontigo (BBVA - Bizum)es.lacaixa.mobile.android.newwapicon (CaixaBank - Bizum)es.openbank.mobile (Openbank - Bizum)User permission required: User must enable notification access in Android settings (one-time setup)
Example notification:
Title: "Bizum recibido"
Text: "Has recibido 100,50 € de María García. Concepto: Elena#142"
Note: This example is based on typical Bizum SMS format. Actual push notification format needs validation with real banking apps. Test data available in knowledge/intelligence/raw_intelligence/ (historical SMS from PoC testing on Pichan/Raspberry Pi). Push notification format may differ slightly from SMS format.
Regex pattern:
function parseBizumNotification(text):
// Pattern: "recibido <amount> € de <name>. Concepto: <reference>"
pattern = /recibido\s+([\d,]+)\s*€\s+de\s+([^.]+)\.\s*Concepto:\s*(.+)/i
match = regex_match(pattern, text)
if not match:
return null
return {
amount: parse_decimal(match[1]), // "100,50" → 100.50
sender_name: match[2].trim(), // "María García"
reference: match[3].trim() // "Elena#142"
}
Variations by bank:
Hypothesis: Bizum is a collaboration between Spanish banks (Red de Servicios de Bizum), so notification format is likely standardized across all participating banks. The variations above may be SMS-era differences that have been unified in push notifications.
Solution (defensive): Multiple regex patterns per bank, try all until match
Pseudocode:
BIZUM_PATTERNS = [
/recibido\s+([\d,]+)\s*€\s+de\s+([^.]+)\.\s*Concepto:\s*(.+)/i,
/Bizum de\s+([^:]+):\s+([\d,]+)\s*€\.\s*Concepto:\s*(.+)/i,
/Recepción Bizum\s+([\d,]+)\s*€\.\s*De:\s+([^.]+)\.\s*Concepto:\s*(.+)/i
]
function parseBizumNotification(text):
for pattern in BIZUM_PATTERNS:
match = regex_match(pattern, text)
if match:
return extract_payment_details(match)
return null // No pattern matched
Testing needed: Validate actual push notification format from BBVA, CaixaBank, Openbank apps. May discover single pattern works for all.
Example notification:
Title: "Transferencia recibida"
Text: "100,50 € de ES12 3456 7890 1234 5678 9012. Concepto: Elena#142"
Regex pattern:
function parseSEPANotification(text):
// Pattern: "<amount> € de <IBAN>. Concepto: <reference>"
pattern = /([\d,]+)\s*€\s+de\s+([A-Z]{2}\d{2}\s*(?:\d{4}\s*){4}\d{4})\.\s*Concepto:\s*(.+)/i
match = regex_match(pattern, text)
if not match:
return null
return {
amount: parse_decimal(match[1]),
sender_iban: match[2].replace(/\s/g, ""), // Remove spaces
reference: match[3].trim()
}
Note: SEPA Instant (SCT Inst) transfers complete in seconds, similar to Bizum. Standard SEPA (SCT) takes 1-3 business days. Most Spanish banks now support SEPA Instant. Parsing approach is identical for both.
Reference: https://www.sepaesp.es/sepa/es/secciones/instrumentos/Transferencia-Inmediata/transferencia-inmediata.html
Fraud detection (SEPA Instant feature “g”): Recipient name verification. If the name in the bank account doesn’t match the payment instruction, sender’s bank shows warning. This is a built-in scam protection feature that complements Asgaya’s fraud detection (María’s bot verifies payment.recipient_name == seller.expected_name).
Example notification:
Title: "Pago recibido"
Text: "2.500.000,00 Bs de 0412-1234567. Ref: Elena#142"
Regex pattern:
function parsePagoMovilNotification(text):
// Pattern: "<amount> Bs de <phone>. Ref: <reference>"
pattern = /([\d.,]+)\s*Bs\s+de\s+([\d-]+)\.\s*Ref:\s*(.+)/i
match = regex_match(pattern, text)
if not match:
return null
return {
amount: parse_decimal(match[1]), // "2.500.000,00" → 2500000.00
sender_phone: match[2],
reference: match[3].trim()
}
Currency conversion needed: Bs → EUR (use exchange rate oracle, Phase 1+)
Market validation: Traditional remittance services charge ~10% fees on EUR→VES corridor. Asgaya can offer 0.5-2% fees (sender fee + seller profit margin), capturing 7-9% savings for users. See knowledge/intelligence/2026-06-26-remittance-quote.md for real market data.
Goal: Get “Elena#142” from payment reference
Challenges:
Pseudocode:
function extractCashAccount(reference):
// Remove extra whitespace
clean = reference.trim()
// Pattern: <name>#<number> (supports multi-word names with spaces/hyphens)
// Examples: "Elena#142", "Bodega Rosa#487", "Farmacia-Caracas#1200"
pattern = /([a-zA-Z][a-zA-Z0-9\-\s]*?)\s*#\s*(\d+)/i
match = regex_match(pattern, clean)
if not match:
return null
// Capitalize first letter of each word
name = capitalize_words(match[1].trim()) // "bodega rosa" → "Bodega Rosa"
number = parseInt(match[2])
return name + "#" + number // "Bodega Rosa#487"
Pattern breakdown:
[a-zA-Z] - Must start with letter[a-zA-Z0-9\-\s]*? - Followed by letters, numbers, hyphens, or spaces (non-greedy)\s*#\s* - Hash separator (required for safety)(\d+) - NumberEdge cases:
Initial approach: Require # separator (strict), reject ambiguous references. Later can relax to support legacy formats if needed.
What: Find unfunded covenant where recipient = Elena#142
⚠️ Illustrative pseudocode: the
electrumQuery(...)method names below are placeholders, not real Electrum RPC methods (there is noblockchain.covenant.list_by_recipient; covenants cannot be enumerated that way). (2026-09-16 — same class as the bulletin-board limitation.)
Pseudocode:
function findCovenantByRecipient(cash_account):
// Resolve Cash Account to BCH address
recipient_address = resolveCashAccount(cash_account)
if not recipient_address:
return null // Cash Account not registered
// Query Electrum for covenants with this recipient
covenants = electrumQuery("blockchain.covenant.list_by_recipient", {
recipient_address: recipient_address,
status: "unfunded" // Only unfunded covenants
})
if covenants.length == 0:
return null // No matching covenant
if covenants.length > 1:
// Multiple matches - use most recent
covenants.sort(by: created_at, desc)
return covenants[0]
Electrum query:
{
"method": "blockchain.covenant.list_by_recipient",
"params": {
"recipient_address": "bitcoincash:qp...",
"status": "unfunded"
}
}
Response:
{
"result": [
{
"covenant_id": "abc123...",
"recipient": "bitcoincash:qp...",
"amount": 100.00,
"buffer": 7.00,
"total_bch": 0.0234,
"expires_at": 1735689600,
"created_at": 1735686000
}
]
}
Decision tree:
function matchPaymentToCovenant(payment):
// 1. Extract Cash Account
cash_account = extractCashAccount(payment.reference)
if not cash_account:
log("No Cash Account in reference: " + payment.reference)
return null
// 2. Find covenant
covenant = findCovenantByRecipient(cash_account)
if not covenant:
log("No covenant for " + cash_account)
return null
// 3. Verify amount matches
expected_amount = covenant.amount + (covenant.amount * 0.005) // 0.5% sender fee
tolerance_percent = 0.01 // 1% tolerance (2× the sender fee)
tolerance_amount = covenant.amount * tolerance_percent
if abs(payment.amount - expected_amount) > tolerance_amount:
if payment.amount < expected_amount:
// Underpayment: REJECT - seller would lose money if we fund
log_warning("Underpayment detected. Expected at least " + (expected_amount - tolerance_amount) + ", got " + payment.amount)
show_notification("Payment too low (€" + payment.amount + " < €" + expected_amount + "). Contact sender.")
return null // Don't fund covenant
else:
// Overpayment: ACCEPT - buyer tipped or rounded up
log("Overpayment accepted: €" + payment.amount + " (expected ~€" + expected_amount + "). Proceeding with funding.")
// Continue to fund covenant (buyer gets less BCH per EUR, their choice)
// 4. Check expiry
if now() > covenant.expires_at:
log("Covenant expired: " + covenant.covenant_id)
return null
return covenant
Amount tolerance: 1% of covenant amount (percentage-based, not fixed EUR)
Expiry check: Don’t fund expired covenants (BCH would be locked forever)
What: Does Isabel have enough BCH to fund covenant?
Pseudocode:
function hasEnoughBCH(required_bch):
wallet_balance = getWalletBalance() // Total BCH in wallet
if wallet_balance >= required_bch:
return true
else:
return false
If insufficient: Buy more from Kraken (see below)
What: Create transaction that spends BCH into covenant script
Pseudocode:
function fundCovenant(covenant):
required_bch = covenant.total_bch
// Select UTXOs
coins = selectCoins(required_bch)
if not coins:
return error("Insufficient BCH")
// Build funding transaction
tx = createTransaction({
inputs: coins.inputs,
outputs: [
{
value: required_bch,
script: covenant.covenant_script // Lock in covenant
},
{
value: coins.change,
script: p2pkh(my_change_address) // Change back to Isabel
}
]
})
// Sign and broadcast
signed_tx = signTransaction(tx)
tx_id = broadcast(signed_tx)
// Notify sender (optional)
sendNostrDM(covenant.sender_pubkey, {
type: "covenant_funded",
covenant_id: covenant.covenant_id,
tx_id: tx_id
})
return tx_id
Cost: ~€0.0002 (BCH transaction fee)
Timing: 10-30 seconds (sign + broadcast + 1 confirmation)
Status: Optional extension, not required for Phase 0
Phase 0 approach: If wallet balance insufficient, show notification to seller (“Received payment but wallet has only X BCH, need Y BCH. Top up wallet to fund covenant.”)
Phase 1+ enhancement: Auto-replenish from exchange (Kraken example below)
Two replenishment strategies:
Which strategy?
Trigger: Wallet balance < required amount for covenant
Flow:
Pseudocode:
function ensureSufficientBCH(required_bch, payment_received_eur):
wallet_balance = getWalletBalance()
if wallet_balance >= required_bch:
return // Already have enough
shortage_bch = required_bch - wallet_balance
shortage_eur = shortage_bch * getCurrentBCHPrice()
// Send fiat to exchange (automated bank payment)
sendFiatToExchange(shortage_eur, source="received_payment")
// Buy BCH on exchange
buyBCHFromExchange(shortage_bch)
// Withdraw to wallet
withdrawToWallet(shortage_bch)
// Wait for BCH to arrive (10-30 minutes)
wait_for_wallet_balance_increase(timeout=30_MINUTES)
Limitation: Requires exchange account with EUR balance or automated bank integration
Trigger: Wallet BCH drops below seller’s maximum transaction size
Flow:
Pseudocode:
function checkReplenishmentThreshold():
wallet_balance_eur = getWalletBalance() * getCurrentBCHPrice()
replenishment_threshold = seller_max_listing_amount // e.g., €500
if wallet_balance_eur < replenishment_threshold:
// Trigger bulk buy
bulk_amount = replenishment_threshold * 2 // Buy 2× threshold for buffer
// Automated bank payment to exchange
sendBankPaymentToExchange(bulk_amount)
// Buy BCH in bulk
buyBCHFromExchange(bulk_amount)
log("Bulk replenishment triggered: buying €" + bulk_amount + " BCH")
Advantage: Lower per-transaction fees (bulk buying), faster covenant funding (wallet always topped up)
Kraken (example below): Low fees (0.16-0.26%), reliable API, supports BCH/EUR pairs, SEPA deposits
Alternatives:
Selection criteria:
Pluggable design: App should support multiple exchanges (user configures their preferred exchange + API keys)
Step 1: Get market price
function getKrakenBCHPrice():
response = http_get("https://api.kraken.com/0/public/Ticker?pair=BCHEUR")
price_eur = parse_float(response.result.BCHEUR.c[0]) // Current price
return price_eur
Step 2: Calculate EUR needed
function calculateEURForBCH(bch_amount):
price = getKrakenBCHPrice()
eur_needed = bch_amount * price
// Add 1% for slippage + fees
eur_with_buffer = eur_needed * 1.01
return eur_with_buffer
Step 3: Place market order
function buyBCHFromKraken(bch_amount):
// Authenticate with API key
api_key = getKrakenAPIKey()
api_secret = getKrakenAPISecret()
// Build order request
order = {
pair: "BCHEUR",
type: "buy",
ordertype: "market",
volume: bch_amount
}
// Sign request (Kraken-specific signature)
signature = sign_kraken_request(order, api_secret)
// Submit order
response = http_post("https://api.kraken.com/0/private/AddOrder",
body: order,
headers: {
"API-Key": api_key,
"API-Sign": signature
}
)
if response.error:
return error("Kraken order failed: " + response.error)
order_id = response.result.txid[0]
return order_id
Step 4: Withdraw BCH to wallet
function withdrawBCHFromKraken(amount, my_address):
api_key = getKrakenAPIKey()
api_secret = getKrakenAPISecret()
withdrawal = {
asset: "BCH",
key: my_address, // Pre-registered withdrawal address
amount: amount
}
signature = sign_kraken_request(withdrawal, api_secret)
response = http_post("https://api.kraken.com/0/private/Withdraw",
body: withdrawal,
headers: {
"API-Key": api_key,
"API-Sign": signature
}
)
if response.error:
return error("Withdrawal failed: " + response.error)
// Wait for BCH to arrive (usually 10-30 minutes)
wait_for_wallet_balance_increase(timeout=30_MINUTES)
return success
Security: API keys stored encrypted, withdrawal addresses pre-registered on Kraken
function parsePaymentNotification(text, app_package):
payment = null
if app_package in BIZUM_APPS:
payment = parseBizumNotification(text)
else if app_package in SEPA_APPS:
payment = parseSEPANotification(text)
else if app_package in PAGOMOVIL_APPS:
payment = parsePagoMovilNotification(text)
if not payment:
log_warning("Failed to parse notification: " + text)
// Store for manual review
save_unparsed_notification(text, app_package)
show_notification("Received payment but couldn't parse. Check manually.")
return null
return payment
Manual review UI: Show unparsed notifications, let user manually enter Cash Account
if not resolveCashAccount(cash_account):
log("Cash Account not registered: " + cash_account)
show_notification("Payment received for " + cash_account + " but not registered. Manual action needed.")
// Don't auto-fund (could be wrong recipient)
if not findCovenantByRecipient(cash_account):
log("No covenant for " + cash_account)
show_notification("Payment received for " + cash_account + " but no matching covenant. Contact sender.")
// Refund fiat? Or wait for sender to create covenant?
Phase 0 approach: Show notification, wait for user to investigate
Phase 0 behavior:
wallet_balance = getWalletBalance()
required_bch = covenant.total_bch
if wallet_balance < required_bch:
shortage = required_bch - wallet_balance
log("Insufficient BCH: have " + wallet_balance + ", need " + required_bch)
// Notify seller (Isabel)
show_notification("Payment received but wallet has only " + wallet_balance + " BCH (need " + required_bch + "). Top up wallet to fund covenant.")
// Save pending funding for manual completion
save_pending_funding(covenant, shortage)
Phase 0+ enhancement: Notify sender via Nostr
// Also notify sender (María) so she can pick a different seller if needed
sendNostrDM(covenant.sender_pubkey, {
type: "insufficient_capacity",
covenant_id: covenant.covenant_id,
seller_cash_account: my_cash_account,
message: "Received your payment but temporarily low on BCH. Will fund within 1 hour or you can select a different seller from bulletin board."
})
Benefit: Sender can make real-time decision (wait for current seller to top up, or pick different seller with capacity). Improves UX without adding protocol complexity.
Phase 1+ with exchange integration:
try:
buyBCHFromExchange(shortage) // Auto-replenish (see Exchange Integration section)
catch ExchangeAPIError:
// Fallback to Phase 0 behavior (notify user)
show_notification("Received payment but can't auto-buy BCH. Manual top-up needed.")
Problem: Two notifications for same payment (bank app + SMS)
Solution: Track funded covenants
function handlePaymentReceived(payment):
covenant = matchPaymentToCovenant(payment)
if not covenant:
return
// Check if already funded
if isCovenantAlreadyFunded(covenant.covenant_id):
log("Covenant already funded, ignoring duplicate notification")
return
// Mark as funded (before actually funding, prevent race)
markCovenantAsFunded(covenant.covenant_id)
fundCovenant(covenant)
Storage: Local database (SQLite) tracks funded covenant IDs
Recommendation for Phase 0: Focus on Android (where passive selling works). iOS can launch with manual workflow, upgrade to bank API in Phase 1+ when volume justifies API integration costs.
Critical: Test with actual bank apps (BBVA, CaixaBank, etc.) on real phones
smsbridge_loop.py:
/knowledge/poc/smsbridge_loop.pyRS026: Android Notifications Architecture
/knowledge/research/RS026_android_notifications.mdUses:
Used by:
Interacts with:
Related mechanism docs:
Status: Phase 0 ✅ Telegram parsing implemented | Phase 1+ 🔨 Bank auto-funding (this doc’s focus)
Updated: 2026-08-14 (status clarified - most of this doc is Phase 1+ design)
Originally written: 2026-06-25 (pre-production, focused on seller auto-funding vision)
Complexity: High (most complex component when fully implemented)
Research: See RS026 (Android notifications), smsbridge_loop.py (early SMS prototype)
Phase 0 implementation: Telegram parameter parsing only (NotificationListenerService for Telegram app)
Phase 1+ scope: Bank notification parsing, seller auto-funding, market price subscription, exchange integration
—
| 🏠 Home | ↑ Android App | 📖 Glossary |