Docs · Security
Security model
What protects a streamer's vault, what we can and cannot do, and what you have to trust us for. Written for people who will check.
The vault program has exactly these instructions: create a vault, claim (attested), claim SPL tokens (attested), bind and freeze a payout address (attested), forward to the bound address (permissionless), request, cancel and finalize a wallet rebind, rotate attestors (timelocked), and pause claims. There is no withdraw, sweep or close instruction, for us or anyone else.
- Vault funds only leave through a claim or a forward, and both only pay the streamer's bound wallet.
- Pausing stops claims; it never moves funds. Deposits keep arriving while paused.
- The protocol treasury receives its share through pump.fun distributions only. It holds no authority over vaults.
- Our servers hold no user funds and no platform tokens (they are revoked immediately after login).
Why it is needed
A Solana program cannot check a Twitch or Kick login, and it cannot read who owns a pump.fun profile. So when a streamer claims or binds a payout address, our backend confirms the proof and signs a 244-byte message: “the owner of this platform account may pay this vault to this wallet, once, before this time”. The program verifies that signature through Solana's built-in Ed25519 program before it releases anything.
How the key is protected
- KMS. On mainnet the attestor key is an Ed25519 key inside a cloud KMS (hardware-backed). It never leaves the HSM and is used for nothing else. The devnet key is kept in a server environment variable; the DEMO key is generated in memory and changes on every server start.
- Rotation timelock. Changing the attestor set takes a proposal, a public event and a 48-hour wait before it can be accepted.
- Public log. Every attestation we issue is published at /trust/attestations with its message hash, vault and time, so misuse would be visible.
- Bound wallet. Once a payout address is bound (by a first claim or a payout binding), a vault only pays that wallet. Changing it takes 72 hours and can be cancelled by the current wallet. A leaked attestor key cannot redirect money from a bound vault.
- Large-claim delay. First claims above a threshold wait in a public pending state, giving time to cancel a fraudulent one.
- Short-lived and single-use. Each attestation expires 5 minutes after issue and carries the vault's current nonce, which the claim increments.
Planned for v2: two independent attestors (ours and one run by a separate party), both required to sign.
No login needed. Your payouts go only to an address you published yourself. A vault's payout address can be bound in five ways (four for Twitch and Kick, two for pump.fun); the full rules are in How streamers get paid.
- 01
Verify with Reclaimexplicit proof · method 4
You log in to Twitch or Kick inside Reclaim's flow. We receive only a zero-knowledge proof of your user ID. No access is granted to us.
Twitch · Kick
- 02
Sign in with Solanaexplicit proof · method 5
You sign a free, readable message with the wallet that owns your pump.fun profile. The message names your payout wallet. No transaction, no funds move.
pump.fun
- 03
Public payout addresspassive evidence · method 2
A Solana address you already show in your Twitch or Kick channel description (or a tip page it links to), or the wallet shown on your pump.fun profile.
All three
- 04
Channel codeexplicit proof · method 3
A short code (like STK-7K3F) you or your manager put in the stream title or channel bio for a moment.
Twitch · Kick
- 05
Loginexplicit proof · method 1
Log in with Twitch or Kick. We read your user ID once and revoke the token.
Twitch · Kick
Evidence
Every observation is stored as a public evidence record: source, URL, time, a hash of the raw snippet, a short text excerpt and the address. Its SHA-256 (the evidence hash) is written into the binding attestation, so the on-chain binding points at exactly what we saw. We read channel descriptions and stream titles only through the official Twitch and Kick APIs, tip pages only when linked from that description, and never scrape. pump.fun has no official public API; we read public profiles through the same endpoints its own website uses (details in For integrators).
pump.fun: signature from the profile wallet
- Who may sign. Only the wallet that owns the pump.fun profile (its
canonical_svm_wallet). We re-read the profile when the signature arrives, not just when the message was issued, so a profile that changed hands in between is refused. - What is signed. A plain-text Sign-In With Solana message that names our domain, the profile and the payout wallet. Changing any of them breaks the signature, so a signature cannot be replayed to bind another wallet.
- Replay. Each message carries a random nonce, is valid for 10 minutes and is accepted once. At most 20 messages per network per hour.
- No transaction. Signing a message cannot move funds or approve anything. Wallets show it as a message, not a transaction.
Freeze
- Published addresses are re-checked about every 6 hours. If the address disappears or changes, or the source fails 3 times in a row, the binding freezes at once.
- A freeze stops payouts and nothing else: funds stay in the vault and the bound address does not change.
- Our attestor can freeze a binding (attestation kind 4) but can never redirect one: the program rejects a freeze whose recipient is not the address already bound.
- Unfreezing takes the same address seen on 2 checks at least 24 hours apart, or explicit proof (Reclaim, profile-wallet signature, code, login).
Auto-forward limits
forward(vault)is permissionless and has no recipient argument: it can only pay the bound wallet, and only while the binding is active and not frozen.- Per-vault rate limit: at most 50 SOL per rolling 24 hours, with a minimum interval between forwards. Anything above waits or is claimed explicitly by the streamer.
- First bindings of vaults holding more than 25 SOL wait 24 hours in public; wallet changes wait 72 hours.
What protects the program itself, beyond its instruction set. Each item shows where it stands today; the program is not deployed yet.
- Planned before mainnet
Attestor key in KMS
On mainnet the Ed25519 attestor key lives in a cloud KMS and never leaves the HSM. Rotating it takes a public proposal and a 48-hour timelock.
- Planned before mainnet
Upgrade authority: multisig with timelock
The program's upgrade authority is a Squads multisig behind a timelock, so no single key can change the code and every upgrade is visible in advance. The plan is to make the program immutable after audits.
- In the program spec
Per-vault rate limits
Withdrawals and auto-forwards are capped per vault per rolling 24 hours, with a minimum interval, so even a bug or a compromised key cannot drain a vault at once.
- Site log live (DEMO)
Public event log
Every attestation and every binding change (bound, pending, frozen, unfrozen, rebind) is published with its hash and time, and emitted as an on-chain event.
- Planned before mainnet
Open source, verifiable build
The program's source will be public, built reproducibly with solana-verify, and the build hash published here so anyone can check that the deployed bytecode matches the code.
A fixed 244-byte binary message (layout v2), little-endian. The domain tag STRMFEES:ATTEST\0 is deliberately brand-neutral so that renaming the product does not change the protocol. The program compares every byte.
user_id is a 32-byte field (with its real length in user_id_len) because the three platforms identify accounts differently. Twitch and Kick use numeric IDs, written as decimal ASCII (up to 20 digits). pump.fun identifies a profile by a UUID; we write it as its 32 lowercase hex characters without dashes, so d0d0a1c3-4f2b-4e6d-8a1f-00000000c001 becomes d0d0a1c34f2b4e6d8a1f00000000c001. The same bytes are the last seed of the vault address.
| Field | Offset | Bytes | Meaning |
|---|---|---|---|
| domain_tag | 0 | 16 | ASCII "STRMFEES:ATTEST\0" |
| version | 16 | 1 | layout version = 2 |
| kind | 17 | 1 | 1 claim, 2 rebind, 3 bind_payout, 4 freeze_payout |
| cluster | 18 | 1 | 0 = mainnet, 1 = devnet, 2 = localnet |
| program_id | 19 | 32 | vault program |
| platform | 51 | 1 | 1 twitch, 2 kick, 3 pump.fun |
| user_id_len | 52 | 1 | 1..32 |
| user_id | 53 | 32 | ASCII, right-padded 0x00 (decimal; pump.fun: 32-hex UUID) |
| vault | 85 | 32 | streamer vault PDA |
| recipient | 117 | 32 | payout wallet (rebind: new wallet) |
| quote_mint | 149 | 32 | System Program id = native SOL |
| nonce | 181 | 8 | u64 LE, must equal vault.claim_nonce |
| expires_at | 189 | 8 | i64 LE unix seconds (issued + 300) |
| issued_at | 197 | 8 | i64 LE unix seconds |
| method | 205 | 1 | 1 oauth, 2 public_address, 3 channel_code, 4 reclaim_zktls, 5 wallet_signature |
| evidence_hash | 206 | 32 | sha256 of the canonical evidence record (zeros for oauth) |
| reserved | 238 | 6 | zeros |
| total | 244 | 244 bytes, signed as-is with Ed25519 |
A claim transaction carries the attestation like this:
0..n pump_fees.distribute_creator_fees_v2 (coins with pending fees, optional)
n+1 Ed25519Program verify (attestor pubkey, 244-byte message, signature)
n+2 vault.claim(amount) (reads the instructions sysvar, checks the message)These properties are tested before any deployment and must hold on every version of the program:
- Nothing except a valid attested claim, or a forward to the bound wallet, can move lamports out of a vault or take it below rent.
- A replayed attestation fails (the nonce must equal the vault's current nonce).
- An expired attestation fails, and so does one for the wrong cluster or the wrong program.
- An attestation for vault A cannot release funds from vault B.
- Once a wallet is bound, a claim or forward to any other recipient fails.
- A freeze attestation can only name the wallet already bound; the attestor cannot redirect a bound vault.
- A forward never exceeds the per-vault rate limit and never runs while the binding is frozen.
- Pausing claims never moves or locks deposits.
pump.fun's global admin can reset a coin's fee-sharing config through its community-takeover process. After a reset, new creator fees may no longer reach the streamer's vault. We cannot prevent it; we detect it. The indexer watches ResetFeeSharingConfigEvent and the coin page shows “fee split reset by pump.fun”. Everything already distributed stays in the vault.
Before mainnet we will decide whether to screen payout wallets against public sanctions lists when issuing an attestation. If we do, a blocked wallet simply cannot receive an attestation; the funds stay in the vault and the streamer can claim to another wallet.
Found a vulnerability in the site, the attestation service or the program? Report it privately first, with steps to reproduce. We will confirm within 72 hours and credit you publicly if you want.