Paranoid Mode docs
PM, token $PM, at paranoid.cash
Paranoid Mode is a mixnet for traffic between AI agents and the model providers they call. It does one job: make it impossible for someone watching the network, including the provider, to link a request to the person who sent it by looking at addresses or timing.
It does this with three things working together:
- Three-hop paths. Every request crosses three relays run by different people in different regions. Each relay only knows the hop before and the hop after.
- Constant-rate traffic. Every client and every relay sends packets of one fixed size at a fixed rate, all the time. Real packets replace cover packets, so the flow never changes shape.
- Staked relays. Relays lock $PM on Ethereum, get paid daily for keeping the rate up, and are slashed if they are caught skipping cover traffic.
The project takes its name and its starting point from the paranoid mode Vitalik Buterin described for his AI agent on October 16, 2026. Read his post. Paranoid Mode is independent and not affiliated with him.
Where it fits
Vitalik's paranoid mode has five layers. This network replaces one of them and leaves the rest as they are.
| Layer | What it hides | Handled by |
|---|---|---|
| Local prompt rewrite | Your writing style | A local model on your machine, unchanged |
| Sandbox | Anything the agent might leak on its own | bubblewrap or a microVM, unchanged |
| zkAPI payments | Who paid for the request | zkAPI, unchanged |
| Cover traffic | When you are active | Paranoid Mode, as constant-rate traffic instead of random extras |
| Tor circuit per chat | Your IP address | Paranoid Mode, as a three-hop mixnet built for this traffic |
Random cover traffic on top of Tor still leaves bursts that an active attacker can find. Moving both jobs into one network built for constant rate is what closes the timing gap he described.
Architecture
There are four parts.
- The client runs on the user's machine next to the agent. It looks like an OpenAI-compatible endpoint on localhost. It picks paths, wraps packets and keeps the constant-rate stream going.
- Relays receive packets, remove one layer, wait a short random time, reorder and forward. Each relay also keeps its own outgoing rate constant by adding cover packets.
- The exit is the third relay on a path. It rebuilds the request from its packets and sends it to the provider over TLS, then sends the answer back the same way in fixed-size packets.
- Ethereum holds the list of relays, their stake, the daily reward pools and the challenge votes. Nothing about individual requests ever goes onchain.
agent ── localhost ──▶ client ══▶ relay 1 ══▶ relay 2 ══▶ relay 3 ── TLS ──▶ provider
│ (eu) (asia) (na)
└─ reads relay list and keys from Ethereum
══ fixed-size packets at a constant rate, three layers of encryption
Packets and timing
Every packet on the network is the same size. A short request might fill one packet and a long answer might take many, but from the outside each one looks identical. Cover packets are encrypted the same way as real ones, so a relay in the middle of a path cannot tell them apart either. Only the client that made a packet knows whether it was real.
Rate
The minimum rate for a relay is one packet per second, every second of the day. That is 86,400 packets per day, and it is the number the rewards contract checks. Busier relays send more, but they never go below the minimum and they never vary with demand.
Mixing delay
Each relay holds a packet for a short random time before sending it on, and sends out packets in a different order than they arrived. This breaks the link between "packet in" and "packet out" even for someone watching one relay closely.
What an observer sees
Someone watching your connection sees a flat stream to one relay, all day. Someone watching the provider sees requests arriving from many exit relays. There is no burst on your side that lines up with an arrival on theirs.
Choosing a path
The client reads every active relay from ParanoidMixRelayRegistry: its endpoint hash, its region and its stake. It then picks three relays with these rules:
- All three must be in different regions.
- Within those limits, a relay's chance of being picked grows with its stake, up to the 250,000 cap.
- A new path is picked for every new conversation, so conversations cannot be linked to each other through the relays they used.
The endpoint hash onchain lets the client check that the onion address and key a relay announces are the ones it registered with. If they do not match, the client skips that relay.
Sending traffic
Point your agent at the local client instead of at the provider. Anything that talks to an OpenAI-compatible API works without other changes.
OPENAI_BASE_URL=http://127.0.0.1:8462/v1
OPENAI_API_KEY=<your zkAPI credential>
The client keeps the cover stream running whether or not the agent is active. Expect a small amount of steady bandwidth use while it is on, and an extra second or two of delay when a conversation starts while the path is built.
Fees for traffic are paid in $PM and collected by ParanoidMixFeeSplitter. Pair this with zkAPI credentials so the payment itself cannot be traced back to you.
Running a relay
What you need
- A server that can stay online all day with steady bandwidth.
- An onion address and an ed25519 public key for the relay.
- At least 50,000 PM in the wallet you will join from.
Joining
- Run the relay for a day first and make sure it never drops below one packet per second.
- Hash the onion address and public key together. The join form on the home page does this for you with SHA-256.
- Approve the registry to take your stake:
approve(registry, amount)on the token. - Call
join(endpointHash, region, amount)on the registry. It returnstrueif you are in.
Region codes
| Code | Region | Code | Region |
|---|---|---|---|
| 1 | North America | 5 | Middle East |
| 2 | South America | 6 | Asia |
| 3 | Europe | 7 | Oceania |
| 4 | Africa |
Leaving
Call beginExit(). Your relay stops being picked right away and your stake stops counting toward rewards. After 14 days call finishExit() to get the stake back. Your stake can still be slashed during those 14 days, which stops a relay from misbehaving and leaving at the same time. You can call cancelExit() at any point before you finish.
If a slash leaves you with less than 50,000, the registry starts your exit for you.
Days and rewards
Rewards work in days counted from the moment the rewards contract was wired. currentEpoch() returns the current day number.
| Day | What happens |
|---|---|
| N | Relays carry traffic. Fees arrive and are added to day N's pool. |
| N+1 | Each relay calls report(N, packets). A report below 86,400 is refused. |
| N+1 to N+2 | Anyone can challenge a day N report. Votes last one day. |
| N+3 onward | Relays call claim(N) to collect their share. |
A relay's share of day N's pool is its weight divided by the total weight of every relay that reported. Its weight is its stake at the time it reported, capped at 250,000. A relay that missed its report window gets nothing for that day.
If nobody earned anything for a day, anyone can call rollover(N) after day N+3 to move that pool into the current day.
Challenges
A relay that quietly drops its cover traffic when nobody is using it saves money and leaks timing for everyone routed through it. Challenges are how the network catches that.
- Open. During day N+1, anyone approves 1,000 PM to the court and calls
open(relay, N, evidenceHash). The evidence itself is published off-chain; the hash pins it. The relay's day N claim is frozen. - Vote. For one day, active relays call
vote(id, support). Each vote counts with the voter's full stake. - Resolve. After the vote closes, anyone calls
resolve(id).
| Outcome | Rule | Result |
|---|---|---|
| Upheld | Votes cast reach 10% of active stake and yes beats no | Day N reward wiped. 20% of stake slashed, half to the challenger, half burned. Bond returned. |
| Rejected | Quorum reached and no is equal to or above yes | Bond goes to the accused relay. Claim unfrozen. |
| No quorum | Votes cast below 10% of active stake | Bond returned. Claim unfrozen. |
What counts as evidence
The useful kind is a capture taken from the relay's neighbours: if a relay's peers saw long gaps in what it sent them, it was not keeping the rate it reported. Voting relays can check this against what they saw themselves, since they were the ones receiving its packets.
Fees and burn
Fees paid for traffic are sent to ParanoidMixFeeSplitter. Anyone can call split(), which sends 85% of the balance to the rewards contract for the current day and burns the other 15%. The split is fixed in the contract and there is no owner who can change it.
Half of every slashed stake is also burned, by the registry.
$PM
| Name | Paranoid Mode |
|---|---|
| Symbol | PM |
| Ticker | $PM |
| Website | paranoid.cash |
| Decimals | 18 |
| Supply | 1,000,000,000, created once by open() |
| Network | Ethereum mainnet |
| Address | pending |
| Extras | EIP-2612 permit, burn, burnFrom, sendMany |
$PM is the token of Paranoid Mode. Relays stake it to join, earn it each day for keeping the rate up, and users pay their traffic fees in it. No owner, no mint after launch, no transfer fee, no pause, no blocklist.
Contracts and wiring
None of the contracts take constructor arguments. Each is deployed bare and then connected once by the deployer with wire. After wiring, the deployer has no further powers. No function in any of the contracts reverts: anything that cannot be done returns false or 0.
1. deploy ParanoidToken, call open()
2. deploy ParanoidMixRelayRegistry
ParanoidMixEpochRewards
ParanoidMixChallengeCourt
ParanoidMixFeeSplitter
3. registry.wire(token, court)
rewards.wire(token, registry, court)
court.wire(token, registry, rewards)
splitter.wire(token, rewards)
| Contract | Job | Address |
|---|---|---|
ParanoidToken | The $PM token | pending |
ParanoidMixRelayRegistry | Relays, stake, exits, slashing | pending |
ParanoidMixEpochRewards | Daily pools, reports, claims | pending |
ParanoidMixChallengeCourt | Challenges and votes | pending |
ParanoidMixFeeSplitter | 85/15 fee split | pending |
Contract reference
ParanoidMixRelayRegistry
| Function | Who | Does |
|---|---|---|
join(bytes32 endpoint, uint16 region, uint256 amount) | anyone | Locks at least 50,000 and registers the relay. Needs approval first. |
addStake(uint256 amount) | relay | Locks more stake. |
beginExit() | relay | Leaves the active set and starts the 14 day wait. |
cancelExit() | relay | Comes back before the wait is over, if stake is still at least 50,000. |
finishExit() | relay | Returns the stake after the wait. |
slash(address relay, uint256 bps, address challenger) | court only | Takes up to 50% of stake, half to the challenger, half burned. |
isActive(address), stakeOf(address) | view | Relay status and stake. |
relays(address), relayAt(uint256) | view | Full relay record, and relays by join order starting at 1. |
activeCount(), totalActiveStake() | view | Size of the active set. |
ParanoidMixEpochRewards
| Function | Who | Does |
|---|---|---|
fund(uint256 amount) | anyone | Adds tokens to today's pool. Needs approval. |
sync() | anyone | Adds tokens that were sent straight to the contract to today's pool. |
report(uint256 epoch, uint256 packets) | active relay | Reports the previous day. Only during the next day, at least 86,400 packets. |
claim(uint256 epoch) | relay | Pays out its share from day N+3 on. Returns the amount. |
rollover(uint256 epoch) | anyone | Moves an unclaimed pool with no earners into today. |
hold, forfeit | court only | Freezes or wipes a relay's share for one day. |
currentEpoch(), claimable(epoch, relay), pool(epoch), weightOf(epoch, relay) | view | Day number, payout, pool size, weight. |
ParanoidMixChallengeCourt
| Function | Who | Does |
|---|---|---|
open(address relay, uint256 epoch, bytes32 evidence) | anyone | Posts the 1,000 bond and opens a case. Returns the case id, or 0. |
vote(uint256 id, bool support) | active relay | Votes once per case with its stake. |
resolve(uint256 id) | anyone | Settles the case after the vote closes. |
cases(uint256), caseFor(epoch, relay) | view | Case details, and the case id for a relay and day. |
ParanoidMixFeeSplitter
| Function | Who | Does |
|---|---|---|
split() | anyone | Sends 85% of its balance to rewards and burns 15%. |
totalToRelays(), totalBurned() | view | Running totals. |
Reading the registry
const reg = new ethers.Contract(REGISTRY, [
"function relayCount() view returns (uint256)",
"function relayAt(uint256) view returns (address)",
"function relays(address) view returns (uint256 stake, uint64 joinedAt, uint64 exitAt, uint16 region, bytes32 endpoint)",
"function isActive(address) view returns (bool)"
], provider);
const n = await reg.relayCount();
for (let i = 1n; i <= n; i++) {
const addr = await reg.relayAt(i);
if (await reg.isActive(addr)) console.log(addr, await reg.relays(addr));
}
Parameters
All of these are constants. None can be changed after deployment.
| Constant | Value | Contract |
|---|---|---|
MIN_STAKE | 50,000 PM | Registry |
EXIT_DELAY | 14 days | Registry |
MAX_SLASH_BPS | 5,000 (50%) | Registry |
EPOCH | 1 day | Rewards |
MIN_PACKETS | 86,400 per day | Rewards |
STAKE_CAP | 250,000 PM | Rewards |
BOND | 1,000 PM | Court |
VOTE_TIME | 1 day | Court |
QUORUM_BPS | 1,000 (10% of active stake) | Court |
SLASH_BPS | 2,000 (20%) | Court |
TO_RELAYS_BPS | 8,500 (85%) | Fee splitter |
Threat model
Protected against
- A provider that wants to know which IP address sent a request.
- A provider working with the first relay on your path to match timing. This is the attack Vitalik pointed out against Tor, and constant rate is the answer to it.
- Someone watching your home connection who wants to know when you use AI.
- Linking two of your conversations through the network, since each one gets a new path.
Not protected against
- The content of a request. The provider has to read it to answer it.
- All three relays on your path being run by the same person. Region rules and stake weighting make this unlikely, not impossible.
- An agent that leaks things on its own. That is what the sandbox layer is for.
- Payment tracing if you pay fees straight from your main wallet. Use zkAPI-style credits.
FAQ
Is this Vitalik's project?
No. It builds the missing piece he described in his post. It is not affiliated with him or the Ethereum Foundation.
Why not just use Tor?
Tor sends traffic when you send traffic. That is what makes it fast for browsing and what makes it weak here: bursts can be lined up. Paranoid Mode is slower to start but sends the same amount at all times.
Does the client use a lot of bandwidth?
It uses a small, steady amount all the time it is on, instead of nothing most of the time and a lot in bursts. That steady amount is the cost of privacy.
Can a relay fake its packet count?
It can write any number in its report, but its neighbours saw what it actually sent. That is what the challenge court is for, and a proven lie costs 20% of its stake.
Who controls the contracts?
Nobody. The deployer connects them once and has no powers after that. Every parameter is a constant.