UNLINKED Open the app

How UNLINKED works

Stealth addresses, shaped after ERC-5564 and ERC-6538. Nothing here is novel. The value is that it is small, checked, and running.

What it is

On Robinhood Chain a wallet is a public brokerage account. Every stock token and memecoin you hold is visible, and so is every payout that lands on you. Each one ties money to your name.

UNLINKED breaks that link. A payer derives a fresh, one-time address from keys you published once. The money lands there. Only you can spend it, and nothing on chain says the address is yours.

This is not anonymity and does not pretend to be. The payer still knows who they paid. Funds are never pooled and never mixed. What disappears is the public link between a payment and its owner.

The four steps

1 · Register

You sign one message. Its hash derives two private keys: a spending key and a viewing key. Their public halves form your meta-address, which you publish once.

2 · Pay

The payer picks a random number, combines it with your viewing public key, and derives a one-time address from your spending public key. Funds go there; the random number's public half becomes an announcement.

3 · Find

You scan announcements with your viewing key. A one-byte tag discards 255 of every 256 strangers' announcements before any curve maths runs.

4 · Spend

Your spending key plus the shared secret gives the private key to that one-time address. The viewing key alone cannot produce it — so a scanning service can find your payments without being able to take them.

Keys

spend = H("posit:spend:" ‖ first half of the signature)  mod n
view  = H("posit:view:"  ‖ second half of the signature) mod n

metaAddress = (spend·G) ‖ (view·G)          // 66 bytes

Deriving from a signature means the keys follow the wallet: the same wallet on another machine reproduces them, and nothing needs a separate backup. The cost is that whoever obtains that one signature obtains both keys, so the message is fixed, specific to UNLINKED, and grants nothing.

Paying

R       = r·G                                // ephemeral key, announced
secret  = H( (r · viewPub).compressed )      // shared secret
stealth = spendPub + H(secret)·G             // one-time public key
address = last 20 bytes of H(stealth.uncompressed)
viewTag = secret[0]                          // one byte

Finding

secret = H( (view · R).compressed )

r·viewPub and view·R are the same point, so both sides reach the same secret without ever talking. If the first byte differs from the announced tag, the announcement is not yours and scanning stops there.

Spending

stealthPrivate = (spend + H(secret)) mod n

Contract surface

function setMetaAddress(bytes calldata metaAddress) external;
mapping(address => bytes) public metaAddressOf;

function sendEth(address stealthAddress, bytes calldata ephemeralPub,
                 bytes1 viewTag) external payable;

function sendToken(address stealthAddress, address token, uint256 amount,
                   bytes calldata ephemeralPub, bytes1 viewTag) external payable;

event Announcement(
    address indexed stealthAddress,
    address indexed token,
    address indexed sender,
    uint256 amount,
    bytes ephemeralPub,
    bytes1 viewTag
);

Funds move straight from payer to the one-time address in the same transaction as the announcement. The contract never holds a balance — a fuzz test asserts that over 256 runs.

Threat model

Hidden from a chain observer: that a given payment belongs to you. Each payment lands on an address with no history and no link to your meta-address.

Not hidden:

Status

PieceState
Stealth address cryptographydone · 9 checks
Contract — registry + announcementsdone · 12 tests incl. fuzz
End-to-end on a real EVMpasses · 7 steps
Web app — register, send, finddone
Deployed to Robinhood Chainnot yet
Relayer for gasless withdrawalnot yet

Until the contract is deployed the app generates real addresses and real keys, but announcements travel out of band instead of on chain.

A stealth address receives tokens but holds no native coin, so it cannot pay for its own withdrawal, and funding it from a linked address undoes the privacy. The answer is a relayer that submits the transaction and takes its fee in the token being moved. That is required by the mechanics, not an add-on.

Run it yourself

# the protocol
node site/test/stealth.test.mjs

# the contract
cd contracts && forge test

# the whole loop against a local chain, seven steps end to end
cd contracts && ./e2e.sh

The cryptography ships with the page instead of loading from a third-party host, because a privacy tool that fetches its own maths from someone else's server is not one.