Protocol Flow
From payment to spendable funds
The protocol has six stages. Each one exists to separate public discovery from private ownership while keeping the result usable in today's wallets.

Stage 1: Generate your keys

The backend generates a hybrid identity — two keypairs of different types:
- Spending keypair (
spending_pub/spending_sk) - a secp256k1 keypair that determines where money lands and, later, spends it - Viewing keypair (
viewing_pk/viewing_sk) - an ML-KEM-768 keypair that lets you find your payments
These are bundled into your meta-address: the public-facing profile that senders use.
curl -s -X POST https://backend.specterpq.com/api/v1/keys/generate | jq .
Response includes meta_address, spending_pub, spending_sk, viewing_pk, viewing_sk, protocol_version.

Stage 2: Publish your meta-address
Make your meta-address discoverable. Two options:
ENS (Ethereum): Upload meta-address to IPFS, set your ENS text record to point at the IPFS CID. Senders resolve alice.eth to your meta-address automatically.
SuiNS (Sui): Same pattern with Sui name records.
Or just share the raw meta-address string directly. Name services are convenient, not required.
Stage 3: Sender creates the payment
This is where ML-KEM does its work. The sender:
- Reads your
viewing_pkfrom the meta-address - Runs ML-KEM encapsulation to produce:
- A ciphertext (1,088 bytes) - this is the encrypted hint
- A shared secret (32 bytes) - known to both sender and recipient
- Computes a 1-byte view tag from the shared secret
- Derives a stealth address (Ethereum and/or Sui) from the shared secret + your
spending_pub
curl -s -X POST https://backend.specterpq.com/api/v1/stealth/create \
-H "Content-Type: application/json" \
-d '{"meta_address":"<RECIPIENT_META_ADDRESS>"}' | jq .
The sender then sends ETH/tokens to the returned stealth_address.

Stage 4: Sender posts an announcement
The announcement is the public breadcrumb. Payments settle on Ethereum, Arbitrum, Monad, or Sui, but every announcement is recorded on Monad Testnet, which serves as the registry chain. It contains:
| Field | Purpose |
|---|---|
payment_id | Server-held pending payment binding from /stealth/create |
tx_hash | Monad announce transaction hash. Required when the relayer is not configured |
payment_tx_hash | Source-chain payment transaction hash (optional) |
source_chain_id | EIP-155 source-chain ID (optional) |
amount | Payment amount (optional) |
chain | Source chain name (optional) |
token | ERC-20 token contract for payment verification (optional) |
curl -s -X POST https://backend.specterpq.com/api/v1/registry/announcements \
-H "Content-Type: application/json" \
-d '{
"payment_id":"<PAYMENT_ID_FROM_STEALTH_CREATE>",
"tx_hash":"0x...",
"amount":"0.1",
"chain":"ethereum"
}' | jq .
Preferred publishes use the server-held shared secret to encrypt payment metadata into a 93-byte blob. The fallback announcement path emits 77-byte plaintext metadata because the server has no shared secret.
Stage 5: Recipient scans
This is the discovery process. The recipient:
- Loads announcements from the registry
- For each announcement, decapsulates the ciphertext using
viewing_sk - Computes the expected view tag from the shared secret
- Fast path: if the view tag doesn't match, skip (filters ~99.6% of announcements)
- Full path: if it matches, derive the stealth address and compare
curl -s -X POST https://backend.specterpq.com/api/v1/stealth/scan \
-H "Content-Type: application/json" \
-d '{
"viewing_sk":"<HEX>",
"spending_pub":"<HEX>"
}' | jq .

Stage 6: Spend the funds
The scan returns everything needed to locate and unlock the funds:
| Field | What it is |
|---|---|
stealth_address | The Ethereum address holding the funds |
stealth_sui_address | The Sui address (if applicable) |
shared_secret | The per-payment ML-KEM secret used to derive the spend key |
The scan is spend-key-free — it never receives or returns a private key. To spend, combine
shared_secret with your spending secret key locally: the one-time private key is
spending_sk + tweak(shared_secret) mod n. Import the derived key into any Ethereum wallet and
spend normally.
viewing_sk and every shared_secret in the scan response are sensitive — a shared secret plus
your spending secret key recovers a spendable key. Treat them like a seed phrase. Never log them,
never expose them in analytics.
The three objects that matter
| Object | What it does | Who holds it |
|---|---|---|
| Meta-address | Public receiving profile | Published by recipient, read by senders |
| Announcement | Encrypted breadcrumb for discovery | Published by sender, scanned by recipient |
| Stealth private key | Controls the funds | Only the recipient |
What sender and recipient each see
- Sender's perspective
- Recipient's perspective
- Observer's perspective
The sender needs only the recipient's meta-address or name.
They never learn the recipient's real wallet address. They derive a fresh stealth address, send funds to it, and post an announcement. From their side, it's three API calls.
The recipient periodically scans the announcement registry with their viewing key.
When they find a match, they recover the private key for that stealth address and can spend the funds whenever they want. The scanning process takes 1-2 seconds for 100k announcements.
An observer sees a payment to a random-looking address and an announcement in the registry.
Without the recipient's viewing key, they can't determine who the payment is for. The ML-KEM ciphertext in the announcement is quantum-resistant.
The security split
The receiving path (stages 1-5) uses ML-KEM-768 and is post-quantum safe.
The spending path (stage 6) produces a secp256k1 key for Ethereum wallet compatibility. That part is classical.
Read Security Boundaries for why this split exists and how it could be resolved.