Decentralised whistleblower protection on Ethereum Sepolia.
Blockchain = trust layer, not database. Claims live on IPFS. Stakes keep every actor honest.
Whistleblowers carry extraordinary risk. They face:
- Retaliation β loss of livelihood, prosecution, or worse.
- No anonymity guarantees β centralised platforms can be subpoenaed, hacked, or shut down.
- Zero financial incentive β leaking information of massive public value earns the source nothing.
- Unverifiable claims β without a neutral validation layer, sensational but false stories flood the market.
Existing solutions (SecureDrop, Signal tips) solve the delivery problem but leave the economic and trust problems entirely unsolved.
ChainWhistle turns the whistleblower pipeline into a permissionless, incentive-aligned protocol:
| Participant | What they do | How they are held accountable |
|---|---|---|
| Whistleblower | Submits an IPFS-pinned claim. No registration. Pseudonymous wallet = pseudonymous identity. | Pays gas. Payout only on verified + published path. |
| Validator Orgs | Stake ETH to join. Vote on claim authenticity via commit-reveal. | Stake slashed 5% if their verdict is contradicted by the oracle cycle. |
| News Outlets | Stake ETH. Bid for the right to break a verified story. | Stake seized + permanent blacklist if they win the bid but never publish. |
| Oracle Nodes | Stake ETH. 5 randomly selected per claim to verify actual publication. | Minority voters slashed 10% (anti-collusion). |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FRONTEND (React + ethers.js) β
β Whistleblower β Validator Org β News Outlet β Oracle Nodeβ
ββββββββββββ¬βββββββ΄βββββββββ¬βββββββββ΄ββββββββ¬ββββββββ΄βββββββ¬βββββββ
β β β β
βΌ βΌ βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ChainWhistle.sol (Ethereum Sepolia) β
β submitClaim() β commitVote() / revealVote() β placeBid() β
β finalizeValidation() β finalizeOracle() β withdraw() β
ββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β ClaimSubmitted event
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LLM / TEE Stub Server (Node.js + Express) β
β β’ Listens for ClaimSubmitted events β
β β’ Fetches active validator orgs + metadata β
β β’ Scores orgs 0-100 via OpenRouter (Claude Haiku) β
β β tag/expertise match, conflict-of-interest exclusion β
β β’ Calls assignValidators(claimId, topN) on-chain β
β β’ Proxies IPFS uploads to Pinata (keeps JWT off the client) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββ
β IPFS via Pinata β
β Claim content β
β Proof files β
β Description CID β
ββββββββββββββββββββ
| Layer | Technology |
|---|---|
| Smart contracts | Solidity 0.8.28, Hardhat, Hardhat Toolbox (Viem) |
| Testnet | Ethereum Sepolia |
| Frontend | React 19, Vite 8, TypeScript, Tailwind CSS v4 |
| Blockchain client | ethers.js v6 |
| Off-chain server | Node.js (ESM), Express, multer |
| LLM scoring | OpenRouter API (default: anthropic/claude-haiku-4-5) |
| Decentralised storage | IPFS via Pinata |
| Wallet | MetaMask (required for all roles) |
SUBMITTED ββ(LLM assigns orgs)βββΊ ASSIGNED
β
βββββββββββββββββββ΄βββββββββββββββββββββββ
β₯ 2/3 VERIFIED < 2/3 VERIFIED
β β
βΌ βΌ
VERIFIED FREE PUBLIC
(bid window opens) (whistleblower gets nothing)
β
βββββββββββ΄βββββββββββ
Winning bid escrowed No bids / timeout
β
βΌ
ORACLE PENDING
(5 random oracles selected)
β
ββββββ΄ββββββββββββββββββββββββββββββ
Oracle majority: Published Oracle majority: Not Published
β β
βΌ βΌ
PUBLISHED Outlet BLACKLISTED
(escrow distributed) failedCycles++
SETTLED If failedCycles < 3 β reopen bids
If failedCycles β₯ 3 β FREE PUBLIC
| Role | Minimum Stake |
|---|---|
| Validator Org | 1 ETH |
| News Outlet | 2 ETH |
| Oracle Node | 0.5 ETH |
When the oracle majority confirms publication:
| Recipient | Share |
|---|---|
| Whistleblower | 60% |
| VERIFIED-voting Validator Orgs | 30% (split equally) |
| Published-voting Oracle Nodes | 10% (split equally) |
| Event | Who gets slashed | Amount |
|---|---|---|
| Oracle minority (voted wrong side) | Oracle node | 10% of stake |
| Outlet fails to publish β oracle cycle fails | VERIFIED-voting orgs | 5% of stake |
| Outlet fails to publish | News outlet | Full stake seized, permanent blacklist |
All slashed funds are redistributed to the platform treasury or majority voters.
Validator orgs are not permissionless β they represent accountable, staked institutions:
- Org calls
applyForValidator(name, description, website, tags)withβ₯ 1 ETHstake. - The LLM server indexes the application; the admin frontend shows it alongside OpenRouter-generated relevance scores.
- Owner calls
approveValidator(org)orrejectValidator(org). - Rejected applicants receive a full stake refund via the pull-withdrawal pattern.
The same two-step flow applies to News Outlets and Oracle Nodes (stake-only, no metadata required).
- Node.js β₯ 20, npm β₯ 10
- MetaMask browser extension
- Sepolia ETH (faucet: sepoliafaucet.com)
git clone https://github.com/your-org/chainwhistle.git
cd chainwhistle
# Smart contracts
cd chain && npm install
# Frontend
cd ../client && npm install
# LLM server
cd ../server && npm installchain/.env
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
SEPOLIA_PRIVATE_KEY=0xYOUR_DEPLOYER_PRIVATE_KEY
LLM_SERVER_ADDRESS=0xYOUR_LLM_SERVER_WALLETserver/.env
RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
LLM_SERVER_PRIVATE_KEY=0xYOUR_LLM_SERVER_PRIVATE_KEY
CONTRACT_ADDRESS=0xDEPLOYED_CONTRACT_ADDRESS
OPENROUTER_API_KEY=sk-or-...
PINATA_JWT=eyJ...client/.env.local
VITE_CONTRACT_ADDRESS=0xDEPLOYED_CONTRACT_ADDRESS
VITE_SERVER_URL=http://localhost:3001cd chain
npx hardhat compile
npx hardhat run scripts/deploy.ts --network sepoliaDeployment info is written to deployment.json at the repo root.
cd server
node index.jsThe server listens for ClaimSubmitted events on-chain and automatically calls assignValidators. It also scans recent blocks on startup to recover any missed events (useful after restarts).
cd client
npm run devOpen http://localhost:5173 and connect MetaMask to Sepolia.
# Compile contracts
npx hardhat compile
# Run tests (uses Hardhat's local network + viem)
npx hardhat test
# Lint Solidity
npx solhint 'contracts/**/*.sol'
# Frontend build
npm run build- Open the app in a MetaMask-enabled browser.
- Click Connect Wallet β MetaMask prompts you to switch to Sepolia (chain ID 11155111).
- Your wallet role is detected automatically from on-chain state (whistleblower, org, outlet, oracle, or admin).
- Click Submit Claim in the navigation.
- Complete the 2-step onboarding form:
- Step 1 β Select a category tag (e.g.,
finance,health,environment). Upload up to 5 proof files. They are uploaded to IPFS via the Pinata proxy server; only the CIDs are stored on-chain. - Step 2 β Write a description. It is also uploaded to IPFS as a separate CID (
descriptionCID).
- Step 1 β Select a category tag (e.g.,
- Confirm the MetaMask transaction.
submitClaim(ipfsHash, tag, proofCIDs, descriptionCID)is called, emittingClaimSubmitted. - The LLM server picks up the event, scores active validator orgs against the tag using OpenRouter, and calls
assignValidatorson-chain within seconds. - Your claim now shows as Assigned with a 2-day commit window.
- Switch to the Admin panel (only visible to the deployer address).
- The Pending Applications tab lists orgs that have called
applyForValidatorwith their stake. - The LLM score panel (via
GET /score/:tag) shows each org's relevance score and reasoning. - Click Approve to call
approveValidator(org)on-chain, or Reject to refund their stake.
chainwhistle/
βββ chain/ # Hardhat project
β βββ contracts/
β β βββ ChainWhistle.sol # Core protocol contract
β βββ scripts/
β β βββ deploy.ts # Deployment script (writes deployment.json)
β βββ test/
β βββ ChainWhistle.ts # Full lifecycle test suite (viem)
βββ client/ # React + Vite frontend
β βββ src/
β βββ App.tsx # Single-page app (all views)
βββ server/ # LLM TEE stub + IPFS proxy
β βββ index.js # Express server + event listener
βββ deployment.json # Written by deploy.ts β consumed by client + server
- Pseudonymous by design. Whistleblower identity is their Ethereum wallet address. There is no registration, email, or IP linkage at the protocol level.
- No on-chain claim content. Only IPFS CIDs are stored on-chain. Claim text and proof files live on IPFS.
- Checks-Effects-Interactions. All state mutations follow CEI ordering. External ETH transfers use a pull-withdrawal pattern (
pendingWithdrawals) to eliminate reentrancy vectors. - Overflow-safe. Solidity
^0.8.xbuilt-in overflow checks throughout. All basis-point arithmetic validated to sum toBPS_DENOM(10,000).
- Randomness improvement: Replace the current pseudo-random oracle selection (block hash seeded) with Chainlink VRF for verifiable fairness.
- Claim confidentiality: Asymmetric encryption of claim content to the winning outlet's public key, so content stays private until the outlet is confirmed and keys can be shared.
- Governance: On-chain DAO for protocol parameter changes (stake minimums, time windows, slash percentages) rather than owner-controlled constants.
- Cross-chain: Deploy on an L2 (Optimism, Base) to reduce gas costs for whistleblowers who may be submitting from low-resource environments.
Built at HackX Β· April 2026
- GitHub: https://github.com/localhostwastaken/sshhh
- Live demo: https://chainwhistle.vercel.app