Skip to main content

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.

SPECTER protocol flow


Stage 1: Generate your keys

SPECTER key generation

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.

Setup flow: the recipient generates a spending keypair and a viewing keypair, then publishes a single meta-address

Setup: key generation produces a secp256k1 spending keypair and an ML-KEM-768 viewing keypair, bundled into one meta-address

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:

  1. Reads your viewing_pk from the meta-address
  2. 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
  3. Computes a 1-byte view tag from the shared secret
  4. 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.

Send flow: the sender encapsulates to the viewing key, derives a one-time stealth address, and publishes an announcement with a view tag

Send: ML-KEM-768 encapsulation creates the shared secret, a one-time stealth address, and a view tag

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:

FieldPurpose
payment_idServer-held pending payment binding from /stealth/create
tx_hashMonad announce transaction hash. Required when the relayer is not configured
payment_tx_hashSource-chain payment transaction hash (optional)
source_chain_idEIP-155 source-chain ID (optional)
amountPayment amount (optional)
chainSource chain name (optional)
tokenERC-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:

  1. Loads announcements from the registry
  2. For each announcement, decapsulates the ciphertext using viewing_sk
  3. Computes the expected view tag from the shared secret
  4. Fast path: if the view tag doesn't match, skip (filters ~99.6% of announcements)
  5. 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 .

Recipient scan flow: filter announcements by view tag, decapsulate the matches, and recover the stealth private key

Scanning: view tags filter roughly 99.6% of announcements, then decapsulation recovers the stealth key

Stage 6: Spend the funds

The scan returns everything needed to locate and unlock the funds:

FieldWhat it is
stealth_addressThe Ethereum address holding the funds
stealth_sui_addressThe Sui address (if applicable)
shared_secretThe 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.

Warning

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

ObjectWhat it doesWho holds it
Meta-addressPublic receiving profilePublished by recipient, read by senders
AnnouncementEncrypted breadcrumb for discoveryPublished by sender, scanned by recipient
Stealth private keyControls the fundsOnly the recipient

What sender and recipient each see

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 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.

Post-quantum crypto details

What ML-KEM-768 actually does under the hood.

Try the full flow

Run every stage against the live API.