Concepts
Scanning Payments
How to detect incoming stealth payments using scanning, batch processing, viewing keys, and payment history.
How scanning works
When someone sends a stealth payment, they publish an ephemeral public key on-chain via an announcement PDA. The receiver must check each announcement to see if it's addressed to them.
For each announcement, the scanner computes an ECDH shared secret using the receiver's viewing key and the published ephemeral key. It then derives what the stealth address should be and compares it to the address in the announcement. If they match, the payment is for this receiver.
Basic scanning
use cloak_sdk::{Scanner, Announcement};
let scanner = Scanner::new(&meta);
// Scan a list of announcements
let announcements: Vec<Announcement> = fetch_from_chain().await?;
let detected = scanner.scan_announcements_list(&announcements)?;
for payment in &detected {
println!("Found payment:");
println!(" Address: {}", payment.stealth_address);
println!(" Amount: {:?}", payment.amount);
println!(" Timestamp: {:?}", payment.timestamp);
}Batch scanning
For large datasets, use batch scanning to process announcements in chunks:
// Process in batches of 1000
let detected = scanner.scan_announcements_batch(&announcements, 1000)?;
// Performance: ~100ms for 1K announcements, ~5-10s for 100KFiltered scanning
Filter announcements by timestamp to only process new ones:
use cloak_sdk::ScannerConfig;
// Only scan announcements after a specific timestamp
let scanner = Scanner::with_config(&meta, ScannerConfig {
program_id: my_program_id,
after_timestamp: Some(1709900000),
..Default::default()
});
let detected = scanner.scan_announcements_list(&announcements)?;Viewing key scanning
Use a ViewingKeyScanner for watch-only wallets. It can detect payments but cannot derive spending keys.
use cloak_sdk::{ViewingKey, ViewingKeyScanner};
// Load viewing key (from file, string, or delegation)
let vk = ViewingKey::load_from_file("viewing-key.json")?;
let scanner = ViewingKeyScanner::new(&vk);
// Same scanning API
let detected = scanner.scan_announcements_list(&announcements)?;
// detected contains payments, but you CANNOT call derive_spend_keypair()Payment history
Track detected payments locally with labels, memos, and spent status:
use cloak_sdk::PaymentHistory;
// Create or load payment history
let mut history = PaymentHistory::load_from_file("history.json")
.unwrap_or_else(|_| PaymentHistory::new());
// Add detected payments
for payment in &detected {
history.add_payment(payment.clone());
}
// Organize with labels and memos
history.set_label(&payment.stealth_address, "Salary March");
history.set_memo(&payment.stealth_address, "Monthly payment");
history.mark_spent(&payment.stealth_address);
// Query
let unspent = history.unspent_payments();
let salary = history.payments_by_label("Salary March");
let total = history.total_balance(); // sum of unspent amounts
// Incremental scanning — only check new announcements
let new_detected = scanner.scan_with_history(&announcements, &history)?;
// Save
history.save_to_file("history.json")?;CLI scanning
# Basic scan
cloak --program-id <PROGRAM_ID> scan
# View-only scan
cloak --program-id <PROGRAM_ID> view-scan --viewing-key viewkey1:...
# Payment history
cloak history
cloak history --unspentPerformance
| Announcements | Time |
|---|---|
| 1,000 | <100ms |
| 10,000 | ~500ms |
| 100,000 | ~5-10s |