Concepts/Scanning Payments

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_addressPubkey

The destination stealth address

timestampi64

Unix timestamp when the announcement was created

Basic Scanning

Use the Scanner to check announcements against your viewing key:

basic_scan.rs
rust
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:

filtered_scan.rs
rust
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.

AnnouncementsApproximate 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:

claim_payment.rs
rust
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