A decentralized, AI-enhanced verification layer that identifies human vs. automated behaviour across EVM and Solana ecosystems. Detection methods are community-submitted and community-curated; an on-device multi-role AI brain produces weighted verdicts in real time.
# Install dependencies (backend + frontend)
npm install
cd frontend && npm install && cd ..
# Configure environment
cp .env.example .env
# Edit .env — minimum required: SOLANA_RPC, FEE_RECIPIENT
# Optional but recommended:
# ETHERSCAN_API_KEY — tx graph analysis on ETH / Base / Arbitrum (free at etherscan.io)
# QVAC_URL — set to http://localhost:11435 to enable Qvac brain (recommended)
# Pull Ollama fallback models (used only when Qvac is unavailable)
ollama pull qwen2.5:1.5b # Learner + Evaluator fallback
ollama pull deepseek-r1:1.5b # Evaluator fallback
ollama pull mixtral # Compiler fallback
# Install Qvac CLI + SDK (primary AI runtime for all 3 brain roles)
npm install -g @qvac/cli @qvac/sdk
# Qvac auto-starts via yarn dev:all. Model config in qvac.config.json (Qwen3-8B by default).
# Start everything (Redis + Ollama + Qvac + backend + frontend)
npm run dev:all # hot-reload (nodemon + vite dev server)
npm run start:all # production build
# Redis must be installed separately:
# sudo apt-get install -y redis-server && sudo systemctl enable --now redis-serverRuns at http://localhost:3000 (backend + built frontend).
Dev frontend: http://localhost:5173
poh/dev/
├── src/
│ ├── server.js Express entry — routes, config endpoint, static
│ ├── routes/
│ │ ├── checker.js POST /checker — scan address against all methods
│ │ ├── methods.js POST /methods/listing, GET/POST /methods/verifyer
│ │ ├── profile.js GET/POST /profile — signup, API keys, rewards, faucet
│ │ ├── abi.js GET /abi/evm, GET /abi/solana (ABI/IDL fetch)
│ │ ├── evm.js POST /evm — raw EVM contract eval
│ │ └── rest.js POST /rest — raw REST eval
│ ├── eval/
│ │ └── evaluator.js Multi-language expression sandbox (JS/Go/Rust/PHP/Java)
│ └── utils/
│ ├── brain.js Multi-role AI brain (Evaluator · Learner · Compiler)
│ ├── txGraph.js Transaction graph analysis — counterparty diversity, timing CV
│ ├── profiles.js Profile storage — API keys, balances, votes, rewards
│ ├── jobQueue.js Async job queue — bulk scans, 2 concurrent jobs, 5 wallets/batch
│ ├── scheduler.js Hourly brain consolidation via node-cron
│ ├── redis.js Response cache (falls back to in-memory)
│ ├── solana.js Solana RPC helpers (balance, SPL token, burn verify)
│ └── evm.js EVM RPC helpers (callContract, toHexSelector)
├── frontend/
│ └── src/components/
│ └── HumanPower.vue Vue 3 SPA — Landing / Scanner / Listing / Votes / Profile
├── data/
│ ├── methods.json Registered detection methods
│ ├── weights.json Per-method AI weights (updated by Learner role)
│ ├── dataset.json Scan + vote training records (Alpaca format)
│ ├── profiles.json User profiles, API keys, balances
│ ├── rewards.json Per-method scan earnings and pending withdrawals
│ ├── method_health.json Per-method pass/fail stats
│ ├── feedback.json Verdict correction records (👍/👎 from users)
│ └── brain_state.md Compiler output — compact system summary (updated hourly)
└── scripts/
├── setup.sh Server provisioning — installs Node, Ollama, Qvac, Redis, builds app
├── deploy.sh One-command deploy to remote server via SSH
├── launch.js Orchestrator: Redis · Ollama · Qvac · backend · frontend
├── start-ollama.js Ensures dedicated Ollama instance on :11434
└── start-qvac.js Ensures Qvac OpenAI-compatible server on :11435
Scans one or more wallet addresses against all registered methods.
| Field | Description |
|---|---|
input |
EVM address, Solana base58, ZNS domain (.eth, .bnb, .defi…), or array |
walletAddress |
Connected wallet (for free-tier tracking) |
apiKey |
API key from profile (alternative to walletAddress) |
txHash |
USDC/USDT payment transaction hash (required for paid scans) |
chainIds |
Optional EVM chain filter, e.g. 1,56 |
csv |
Multipart CSV upload with address column (bulk mode) |
Single address → synchronous { result, count, brainKey, freeScansLeft }.
Multiple addresses / CSV → async { jobId, status: "queued", total, pollUrl }.
Poll the status of a bulk scan job. Retained for 2 hours after completion.
{
"jobId": "...",
"status": "queued | running | done",
"total": 1000,
"done": 423,
"percent": 42,
"results": [...],
"errors": [...]
}Concurrency limits: 2 jobs run simultaneously; within each job, 5 wallets are processed in parallel. Up to 10 concurrent API requests map onto 2 running jobs — additional jobs queue automatically.
// Poll until complete
async function pollJob(jobId) {
while (true) {
const job = await fetch(`/checker/job/${jobId}`).then(r => r.json())
if (job.status === 'done') return job.results
await new Promise(r => setTimeout(r, 3000))
}
}Poll for the async AI verdict after a single-wallet scan. brainKey is returned by POST /checker.
{
"status": "done",
"verdict": "HUMAN | AI | UNCERTAIN",
"confidence": 0.0,
"reasoning": "...",
"signal_contributions": { "method_name": 0.0 },
"conflicts": []
}Returns cost breakdown for a given batch size before committing. Pricing is flat: $1 per 1,000 scans ($0.001/scan), paid in USDC or USDT on Solana. First 100 scans per wallet are free.
{ "count": 1000, "perAddress": 0.001, "total": 1000000, "currency": "USDC/USDT", "tiers": [...] }Register a new detection method. Costs 0.01 SOL per method.
Supported types: evm · solana · rest
Returns all registered methods for the community voting queue.
Community vote on a method. Vote weight is proportional to POH token stake. Uses ed25519 signMessage — no on-chain transaction required.
| Field | Type |
|---|---|
methodId |
string |
type |
description | method | risk |
vote |
boolean |
walletAddress |
string |
signature |
string — base58 ed25519 signature of poh-vote-v1:{methodId}:{vote}:{timestamp} |
message |
string — the signed message |
feedback |
string (optional) — LLM-validated before storing |
LLM pre-check for vote feedback and method descriptions. Rejects gibberish, spam, and off-topic content.
{ "feedback": "your comment", "context": "vote | description" }
→ { "valid": true | false, "reason": "..." }Fetches verified ABI from Etherscan → Sourcify fallback. Returns function list with input/output types for the listing UI picker.
Fetches Anchor IDL from apr.dev registry or on-chain IDL account.
Create or update a profile. Requires ed25519 signature of poh-profile-v1:{address}:{timestamp}.
Returns { profile: { apiKey, balance, freeScansLeft, totalScans, stakedAmount } }.
Returns profile stats, submitted methods, and reward totals.
Returns the wallet's full vote history with method descriptions and feedback.
Credit profile balance from a verified POH token transfer.
Withdraw off-chain balance + scan earnings as on-chain POH tokens.
Rotate the API key for a profile.
Leaderboard — top 20 method earners by total POH earned.
Devnet only. Sends 10 000 POH to the caller. 24-hour cooldown per address.
POH runs a multi-role brain entirely on-device. All three roles route through Qvac as primary, with Ollama as automatic fallback if Qvac's circuit breaker opens.
Qvac is Tether's on-device OpenAI-compatible inference server. POH ships with qvac.config.json pre-configured for Qwen3-8B Q4_K_M — downloaded automatically on first start (~4.7 GB).
# Already handled by yarn dev:all / yarn start:all.
# Manual start:
yarn qvac
# .env:
QVAC_URL=http://localhost:11435
QVAC_MODEL=evaluatorConfig (qvac.config.json):
{
"serve": {
"models": {
"evaluator": {
"model": "QWEN3_8B_INST_Q4_K_M",
"default": true,
"preload": true,
"config": { "ctx_size": 8192 }
}
}
}
}Any OpenAI-compatible server works in place of Qvac (LM Studio, llama.cpp ≥ b3670, Ollama with /v1 prefix).
ollama pull qwen2.5:1.5b # Learner + Evaluator fallback
ollama pull deepseek-r1:1.5b # Evaluator fallback
ollama pull mixtral # Compiler fallbackOllama is used automatically only when Qvac is unavailable (circuit breaker open after 3 consecutive failures, retries every 5 minutes).
Called after every scan. Uses strict signal interpretation — no free-form guessing.
- Sends all passing signals + top-10 failing signals by weight (compact format)
- Scores each signal contribution, detects conflicts between signals
- Weak or conflicting signals → outputs
UNCERTAINinstead of a false positive - Runs a second verification pass: checks overconfidence (>0.85 requires strong multi-signal support) and ignored weights
- Injects the last 5 user verdict corrections into the prompt to learn from past mistakes
Output schema:
{
"verdict": "HUMAN | AI | UNCERTAIN",
"confidence": 0.82,
"signal_contributions": { "ETH Balance": 0.4, "ENS Name": 0.3 },
"conflicts": [],
"reasoning": "strong on-chain activity across 3 independent signals"
}Called on every community vote and every user verdict correction (👍/👎).
- Adjusts per-method weights gradually — max ±0.05 per vote
- Weights clamped to 0.1–3.0; no single action can cause large drift
- Records corrections to
data/feedback.json; appends insights tobrain_state.md - User 👎 feedback identifies misleading signals (−0.03 weight); 👍 reinforces them (+0.02)
Runs hourly via scheduler. Rewrites data/brain_state.md.
- Reads top/weak methods by weight, last 8 scans, last 8 votes
- Outputs a ≤400-word technical summary — no speculation, no repetition
Called when a listing is submitted. Evaluates signal quality and edge-case risk (none | low | medium | high). Result is appended to brain_state.md.
All brain calls use evaluatorChatJSON() / learnerChatJSON() / compilerChat():
- Extracts the first
{...}block from model output (handles<think>blocks and markdown fences) - Validates required fields are present
- On failure, retries once with explicit field list
Every scan automatically runs src/utils/txGraph.js in parallel with the registered detection methods. It fetches the last 50 transactions and computes:
| Metric | Signal |
|---|---|
uniqueCounterparties |
Low count → bot (interacts with few addresses) |
repeatRatio |
High → bot (same counterparties over and over) |
timingCv |
Low (< 0.3) → bot (regular intervals = scripted) |
selfRatio |
High → bot (self-transfers, wash activity) |
A result is true (human signal) when: ≥ 8 unique counterparties, repeat ratio < 50%, self-transfer ratio < 30%, timing CV > 0.3.
Data sources:
| Chain | Source |
|---|---|
| ETH | Etherscan v2 API (ETHERSCAN_API_KEY) |
| Base | Etherscan v2 API (ETHERSCAN_API_KEY) |
| Arbitrum | Etherscan v2 API (ETHERSCAN_API_KEY) |
| BNB | Alchemy getAssetTransfers (RPC_56) |
| Solana | getSignaturesForAddress + getTransaction via SOLANA_RPC |
Results appear in the scan output as tx_graph_eth, tx_graph_base, etc. and are included in the brain's verdict context.
Methods use expressions evaluated in a sandboxed VM. Available variables:
| Variable | Type | Description |
|---|---|---|
result |
any[] |
ABI-decoded return values (EVM) |
data |
object |
Parsed response body (REST) |
status |
number |
HTTP status code (REST) |
decimals |
number |
Configured decimals (default: 18 EVM, 9 Solana) |
result[0] > 0n // BigInt token balance > 0
result[0] / 10n ** BigInt(decimals) > 1 // normalised balance > 1
data.has_active_stake === 1 // REST field check
data.totalCount > 0 // NFT countSupported languages: JS · Go · Rust · PHP · Java (all normalised to JS sandbox internally)
| Layer | Tech |
|---|---|
| Backend | Node.js · Express · ethers.js v6 · @solana/web3.js |
| Frontend | Vue 3 · Vite · Lucide icons |
| AI (all roles — primary) | Qvac 0.3.0 · Qwen3-8B Q4_K_M · 8192 ctx |
| AI (all roles — fallback) | Ollama — qwen2.5:1.5b · deepseek-r1:1.5b · mixtral |
| Tx graph | Etherscan v2 (ETH · Base · Arbitrum) · Alchemy (BNB) · Solana RPC |
| Cache | Redis (in-memory fallback) |
| Scheduler | node-cron (hourly brain consolidation) |
| Wallet | @solana/wallet-adapter (Phantom · Solflare · Coinbase · Trust · Ledger · Torus · Nightly + Wallet Standard) |
| Multi-chain | 32 EVM chains via Alchemy + built-in RPC registry |
- Telegram: https://t.me/poh_network_group
- X / Twitter: https://x.com/poh_network