Private prediction markets, step by step.
Polyshield uses ZK proofs to hide which depositor placed which bet. The vault acts as a single Polymarket account shared by all depositors. Here's the full flow.
Deposit USDC into the shared vault
You transfer USDC to the Vault contract. A local spending note (secret, balance, nonce, owner_address) is generated in your browser. The secret is derived from a wallet signature — you never need to back anything up. The Poseidon commitment of that note becomes a new leaf in the vault's Merkle tree. Your deposit amount is public; the note contents are not.
C = Poseidon4(secret, balance, nonce, owner_address)
Choose a Polymarket position
Browse markets and pick a side. Your browser generates a ZK proof (BET_AUTH) proving: (a) your note has sufficient balance, (b) the nullifier belongs to your note, (c) the new note after spending is correctly formed. No secret ever leaves your device.
nullifier = poseidon(secret, nonce)
Proof relay submits the authorization
Your ZK proof goes to the Proof Relay — not your wallet. The relay submits Vault.authorizeBet() from its own EOA. Your wallet address never appears in any bet-related transaction. Gas is paid by the relay.
Vault.authorizeBet(proof, publicInputs)
Vault EOA submits the CLOB order
The Signing Layer detects the BetAuthorized event and submits a Fill-Or-Kill order to Polymarket's CLOB using the vault's single EOA. All traders in the pool share this one address. No CLOB observer can tell which depositor authorized which bet.
POST /order { tokenId, price, size, type: "FOK" }Settle winnings as a new private note
When a market resolves, you generate a SETTLE_CRED proof locally. This proof binds your original position to the market outcome without revealing your note's identity. The settlement credit becomes a fresh private note in your vault.
new_commitment = Poseidon4(secret, balance + credit, nonce+1, owner_address)
Withdraw to your own address
The WITHDRAW proof proves you know a note's secret, and commits to a recipient address via its Poseidon hash (a private input). The relay submits the withdrawal. You can only withdraw to the wallet that made the original deposit — this is enforced inside the ZK circuit via the owner_address field. The link from deposit to withdrawal is still unlinkable on-chain because no identifying data appears in the withdrawal transaction.
recipient_hash = poseidon(recipient_address, 0)
| Threat | Mitigated? | How |
|---|---|---|
| Observer sees which EOA placed the Polymarket order | ✓ YES | All orders from vault's single EOA; depositor never appears |
| On-chain observer links nullifier to depositor address | ✓ YES | Nullifier = poseidon(secret, nonce); not derivable without secret |
| Relay learns which depositor authorized which bet | ✓ YES | Relay only sees the ZK proof; public inputs contain no depositor ID |
| Timing correlation between deposit and withdrawal | ✓ YES | Relay adds random jitter (3–60 min depending on posture) |
| Deposit amount is private | ✗ NO | Vault.deposit() is a public ERC-20 transfer; amount is on-chain |
| That a wallet used Polyshield at all | ✗ NO | Vault.deposit() is public — only post-deposit activity is private |