Scanning Payments
How to efficiently detect incoming stealth payments using the on-chain announcement registry and the Scanner API.
The Challenge
Stealth addresses create a unique problem: recipients don't know which addresses belong to them until they check. Each payment uses a fresh address that looks random to everyone except the intended recipient.
How Detection Works
Senders publish an ephemeral public key alongside each payment. Recipients use their viewing key to perform an ECDH operation with each ephemeral key. If the result matches the stealth address, the payment is theirs.
Announcement Registry
The Onyx Anchor program maintains an on-chain registry of payment announcements. Each announcement contains:
ephemeral_pubkey[u8; 32]The sender's one-time public key for this payment
stealth_addressPubkeyThe destination stealth address
timestampi64Unix timestamp when the announcement was created
Basic Scanning
Use the Scanner to check announcements against your viewing key:
use onyx_sdk::prelude::*;
// Load your meta-address
let meta = StealthMetaAddress::load_from_file("~/.onyx/keys.json")?;
// Create a scanner
let scanner = Scanner::new(&meta);
// Fetch announcements from the registry (RPC call)
let announcements = fetch_announcements(&rpc_client)?;
// Scan for payments meant for you
let my_payments = scanner.scan(&announcements)?;
for payment in my_payments {
println!("Found payment!");
println!(" Address: {}", payment.stealth_address);
println!(" Amount: {} SOL", payment.balance / LAMPORTS_PER_SOL);
}Filtered Scanning
For efficiency, you can filter announcements by timestamp to only scan new payments since your last check:
use onyx_sdk::prelude::*;
use std::time::{SystemTime, UNIX_EPOCH};
let meta = StealthMetaAddress::load_from_file("~/.onyx/keys.json")?;
let scanner = Scanner::new(&meta);
// Only scan announcements from the last 24 hours
let since = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_secs() as i64 - 86400;
let my_payments = scanner
.since(since)
.scan(&announcements)?;
println!("Found {} new payments", my_payments.len());Scanning Performance
Each announcement requires one ECDH operation to check. This is computationally inexpensive but scales linearly with the number of announcements.
| Announcements | Approximate Time |
|---|---|
| 1,000 | < 100ms |
| 10,000 | < 1s |
| 100,000 | ~5-10s |
Performance Tip
Store the timestamp of your last scan and use filtered scanning to only check new announcements. This keeps scan times constant regardless of total registry size.
After Detection
Once you've detected a payment, you can derive the spending keypair and claim the funds:
use onyx_sdk::prelude::*;
let meta = StealthMetaAddress::load_from_file("~/.onyx/keys.json")?;
let scanner = Scanner::new(&meta);
// Scan and find payments
let payments = scanner.scan(&announcements)?;
for payment in payments {
// Derive the keypair for this specific stealth address
let keypair = StealthKeypair::derive(&meta, &payment.ephemeral_pubkey)?;
// Verify the address matches
assert_eq!(keypair.address(), payment.stealth_address);
// Get Solana keypair to sign transactions
let solana_kp = keypair.to_solana_keypair()?;
// Now you can transfer funds out
let destination = Pubkey::from_str("your-wallet-address")?;
transfer(&solana_kp, &destination, payment.balance)?;
}CLI Usage
The Onyx CLI provides a simple interface for scanning:
# Scan for all payments
onyx scan
# Scan since a specific date
onyx scan --since "2024-01-01"
# Scan with verbose output
onyx scan --verbose