How to run, join, and operate on the UNFED AI test network.
For contributor onboarding and PR expectations, see CONTRIBUTING.md.
# Show common tasks
make help
# Local contributor checks
make check
# Public testnet lifecycle helpers
make public-testnet-start
make public-testnet-status
make public-testnet-stop- Python 3.12+
tmux(recommended for multi-process startup on VPS)- Model weights: run
python -m tools.cli split Qwen/Qwen2.5-0.5B -o shards --text-shards 4or download pre-split shards
Install dependencies:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtSuggested minimums for public testnet reliability:
| Node Type | CPU | RAM | GPU | Storage | Notes |
|---|---|---|---|---|---|
| Registry | 1 vCPU | 1 GB | No | 10 GB SSD | Budget profile; for higher node counts use 2 vCPU / 2-4 GB |
| Daemon | 2 vCPU | 4 GB | No | 20 GB SSD | SQLite share-chain + telemetry; use persistent disk |
| MPC Node A | 4 vCPU | 8-16 GB | Recommended | 30 GB SSD | Entry MPC node; should be the stronger of the pair |
| MPC Node B | 4 vCPU | 8-16 GB | Recommended | 30 GB SSD | MPC peer; keep network latency low to Node A |
| Compute (text shard) | 4+ vCPU | 8-16 GB | Recommended | 40+ GB SSD | One process per shard index |
| Vision shard | 4+ vCPU | 12-24 GB | Yes (preferred) | 40+ GB SSD | Needed for multimodal image paths |
| Web UI | 1 vCPU | 1 GB | No | 10 GB SSD | Usually co-located with registry; bind to 127.0.0.1 |
Start a full local stack with on-chain admission + MPC-required policy:
# 0) Prepare local chain + contracts (writes deployed.env)
export OPERATOR_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
export OPERATOR_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
./scripts/start_local_chain.sh
source deployed.env
# 1) Create cluster config from deployed addresses
cat > cluster_config.json <<EOF
{
"name": "Local UNFED Pool",
"chain_rpc_url": "${CHAIN_RPC_URL}",
"escrow_contract_address": "${ESCROW_ADDRESS}",
"staking_token_address": "${TOKEN_ADDRESS}",
"operator_private_key": "${OPERATOR_PRIVATE_KEY}",
"daemon_required_count": 1
}
EOF
# 2) Fund + stake daemon/MPC/compute identities (required in on-chain mode)
FUND_WEI=200000000000000000000
STAKE_WEI=100000000000000000000
for A in \
0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc \
0x976EA74026E726554dB657fA54763abd0C3a0aa9 \
0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 \
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC \
0x90F79bf6EB2c4f870365E785982E1f101E93b906; do
cast send "$TOKEN_ADDRESS" "transfer(address,uint256)" "$A" "$FUND_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key "$OPERATOR_PRIVATE_KEY"
done
# approve + stake (daemon, MPC-B, MPC-A, shard0-3)
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
cast send "$TOKEN_ADDRESS" "approve(address,uint256)" "$ESCROW_ADDRESS" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 && cast send "$ESCROW_ADDRESS" "stake(uint256)" "$STAKE_WEI" --rpc-url "$CHAIN_RPC_URL" --private-key 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
# 3) Start services in strict order (one shell each, or use tmux)
# 3a Registry
python -m network.registry_server --port 50050 --cluster-config cluster_config.json
# 3b Daemon (staked identity)
export UNFED_STAKE_EVM_PRIVATE_KEY=0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba
python -m network.daemon_node --port 50070 --registry localhost:50050 --eth-address 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
# 3c MPC B first, then MPC A
export UNFED_STAKE_EVM_PRIVATE_KEY=0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
python -m network.mpc_shard0 --role B --port 50063 --peer localhost:50061 --registry localhost:50050 --eth-address 0x976EA74026E726554dB657fA54763abd0C3a0aa9 --shards-dir shards
export UNFED_STAKE_EVM_PRIVATE_KEY=0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a
python -m network.mpc_shard0 --role A --port 50061 --peer localhost:50063 --registry localhost:50050 --eth-address 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 --shards-dir shards
# 3d Compute shards
export UNFED_STAKE_EVM_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
python -m node.server --shard-index 0 --port 50051 --shards-dir shards --registry localhost:50050 --eth-address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
export UNFED_STAKE_EVM_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
python -m node.server --shard-index 1 --port 50052 --shards-dir shards --registry localhost:50050 --eth-address 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
export UNFED_STAKE_EVM_PRIVATE_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
python -m node.server --shard-index 2 --port 50053 --shards-dir shards --registry localhost:50050 --eth-address 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
export UNFED_STAKE_EVM_PRIVATE_KEY=0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
python -m node.server --shard-index 3 --port 50054 --shards-dir shards --registry localhost:50050 --eth-address 0x90F79bf6EB2c4f870365E785982E1f101E93b906
# 4) Verify model readiness
python -m client.client --list-models --registry localhost:50050
# 5) Run client query
python -m client.client --registry localhost:50050 --prompt "Hello, world"
# 6) Optional web dashboard
python -m web.server --host 127.0.0.1 --port 8080 --registry localhost:50050By default, the dashboard binds to 127.0.0.1. Use --host 0.0.0.0 only when you
intend to expose it behind network controls.
If you prefer a single copy/paste launch path, run this after chain deploy + staking:
# from repo root
source .venv/bin/activate
# clean old sessions (safe if missing)
tmux kill-session -t registry 2>/dev/null || true
tmux kill-session -t daemon 2>/dev/null || true
tmux kill-session -t mpcb 2>/dev/null || true
tmux kill-session -t mpca 2>/dev/null || true
tmux kill-session -t node0 2>/dev/null || true
tmux kill-session -t node1 2>/dev/null || true
tmux kill-session -t node2 2>/dev/null || true
tmux kill-session -t node3 2>/dev/null || true
tmux kill-session -t web 2>/dev/null || true
# 1) registry
tmux new -s registry -d 'cd '"$PWD"' && source .venv/bin/activate && python -m network.registry_server --port 50050 --cluster-config cluster_config.json'
# 2) daemon
tmux new -s daemon -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba python -m network.daemon_node --port 50070 --registry localhost:50050 --eth-address 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc'
# 3) MPC B then MPC A
tmux new -s mpcb -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e python -m network.mpc_shard0 --role B --port 50063 --peer localhost:50061 --registry localhost:50050 --eth-address 0x976EA74026E726554dB657fA54763abd0C3a0aa9 --shards-dir shards'
tmux new -s mpca -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a python -m network.mpc_shard0 --role A --port 50061 --peer localhost:50063 --registry localhost:50050 --eth-address 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 --shards-dir shards'
# 4) compute shards
tmux new -s node0 -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 python -m node.server --shard-index 0 --port 50051 --shards-dir shards --registry localhost:50050 --eth-address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
tmux new -s node1 -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d python -m node.server --shard-index 1 --port 50052 --shards-dir shards --registry localhost:50050 --eth-address 0x70997970C51812dc3A010C7d01b50e0d17dc79C8'
tmux new -s node2 -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a python -m node.server --shard-index 2 --port 50053 --shards-dir shards --registry localhost:50050 --eth-address 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC'
tmux new -s node3 -d 'cd '"$PWD"' && source .venv/bin/activate && UNFED_STAKE_EVM_PRIVATE_KEY=0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 python -m node.server --shard-index 3 --port 50054 --shards-dir shards --registry localhost:50050 --eth-address 0x90F79bf6EB2c4f870365E785982E1f101E93b906'
# optional web
tmux new -s web -d 'cd '"$PWD"' && source .venv/bin/activate && python -m web.server --host 127.0.0.1 --port 8080 --registry localhost:50050'
# check sessions
tmux lsUseful follow-ups:
# verify readiness
python -m client.client --list-models --registry localhost:50050
# tail one service
tmux attach -t mpca
# stop all local sessions
tmux kill-session -t registry; tmux kill-session -t daemon; tmux kill-session -t mpcb; tmux kill-session -t mpca; tmux kill-session -t node0; tmux kill-session -t node1; tmux kill-session -t node2; tmux kill-session -t node3; tmux kill-session -t webRun preflight before exposing services:
python -m scripts.testnet_preflight web --host 127.0.0.1
python -m scripts.testnet_preflight node --advertise "203.0.113.42:50051" --tls-cert /path/to/server.crt --tls-key /path/to/server.key
python -m scripts.testnet_preflight runtime --web-url http://127.0.0.1:8080Or run the bundled one-command checklist against an env profile:
cp .env.public-testnet.example .env.testnet
./scripts/run_public_testnet_checklist.shOne-command launch + readiness check:
./scripts/start_public_testnet.sh
# optionally reuse already running services
./scripts/start_public_testnet.sh --reuse-runningUNFED helper scripts now support GGUF inspection and conversion. This is useful when you downloaded a model with Ollama and want to validate compatibility before testnet rollout.
Install/update dependencies:
source .venv/bin/activate
pip install -r requirements.txtInspect a GGUF file:
python -m tools.cli inspect /path/to/model.ggufConvert GGUF to a local safetensors file:
python -m tools.cli convert /path/to/model.gguf -o ./model.safetensorsSplit directly from GGUF (supported subset):
python -m tools.cli split /path/to/model.gguf -o ./shards --text-shards 4Current limitations:
- Split from GGUF is text-only and expects transformer-style tensor names (
model.layers.<idx>.*) - Quantized GGUF tensors (for example Q4/Q5/Q8 packed tensors) are rejected
- For unsupported GGUF key layouts, use original Hugging Face weights for splitting
Troubleshooting:
Unsupported GGUF tensor dtype: your file is quantized; use non-quantized export or HF weightssupported transformer layer key names: GGUF uses llama.cpp naming; split from HF source insteadGGUF support requires the 'gguf' package: runpip install gguf
To expose your node to other participants over the internet:
Each service needs its port accessible from the internet:
| Service | Default Port | Protocol |
|---|---|---|
| Registry | 50050 | TCP/gRPC |
| Node | 50051+ | TCP/gRPC |
| Web UI | 8080 | TCP/HTTP |
| Daemon | 50070 | TCP/gRPC |
Example (ufw):
sudo ufw allow 50051/tcpNodes register with the registry using their advertised address. By default this is localhost:<port>, which only works locally.
python -m node.server \
--shard-index 0 \
--port 50051 \
--advertise "203.0.113.42:50051" \
--registry "registry.unfed.ai:50050" \
--shards-dir shardsOr via config file:
python -m node.run --config node_config.jsonSee node_config.testnet.json.example for a template.
Edit network/seeds.json (or set UNFED_SEEDS env var) to point at the public registry:
export UNFED_SEEDS="registry.unfed.ai:50050,registry2.unfed.ai:50050"Or copy network/seeds.testnet.json.example to network/seeds.json and fill in real addresses.
Generate or obtain a TLS certificate, then start with:
python -m network.registry_server \
--port 50050 \
--tls-cert /path/to/server.crt \
--tls-key /path/to/server.key
python -m node.server \
--shard-index 0 --port 50051 \
--tls-cert /path/to/server.crt \
--tls-key /path/to/server.key \
--registry "registry.unfed.ai:50050"Publicly advertised compute nodes now require TLS by default. If a node advertises a non-local endpoint and no TLS cert/key is provided, startup fails closed.
Output privacy now supports full output-stage 2PC without the retired
server_sample path:
off: standard plaintext samplingdecode_client_sample: final node returns encrypted top-k artifact for client-side samplingfull_output_2pc: output stage uses MPC A/B roles for sampled-token artifact return
Set it via env or node config:
export UNFED_HE_COMPUTE_MODE=full_output_2pcserver_sample is rejected at runtime and should not be used. When MPC policy is enabled, model readiness requires one healthy input MPC pair and one healthy output MPC pair (the same A/B pair may advertise both capabilities).
Performance-oriented transport paths for compute/MPC nodes are now always-on:
- daemon share submission uses async queue + batching + compact blob transport
- output-stage 2PC uses binary artifact transport with FP16 hidden-vector codec
- same-next-hop forwarding uses short-window micro-batching (
BatchForward)
These are no longer controlled by runtime environment toggles.
Clients connect with:
python -m client.client --tls-ca /path/to/ca.crtFor testing, generate a self-signed cert:
openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt \
-days 365 -nodes -subj "/CN=your.domain.com" \
-addext "subjectAltName=DNS:your.domain.com,IP:203.0.113.42"The registry can enforce staking, escrow, and settlement on-chain via an ERC-20 token and the UnfedEscrow contract. This is optional — without it the registry runs in-memory simulation mode (--no-chain).
You need Foundry installed and a wallet funded with testnet ETH (get Sepolia ETH from sepoliafaucet.com).
export CHAIN_RPC_URL="https://rpc.sepolia.org"
export OPERATOR_PRIVATE_KEY="0x<your-deployer-key>"
export OPERATOR_ADDRESS="0x<your-deployer-address>"
./scripts/deploy_testnet.shThis deploys UnfedToken (1M test UNFED) and UnfedEscrow, then writes the addresses to deployed.env at the project root.
For local development with Anvil instead:
export OPERATOR_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
export OPERATOR_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
./scripts/start_local_chain.shCreate a cluster_config.json:
{
"name": "My UNFED Pool",
"chain_rpc_url": "https://rpc.sepolia.org",
"escrow_contract_address": "0x<from deployed.env>",
"staking_token_address": "0x<from deployed.env>",
"operator_private_key": "0x<your-operator-key>"
}You do not need to provide a token name in config; the registry resolves token
metadata from the ERC-20 contract at staking_token_address.
Then start the registry:
python -m network.registry_server --port 50050 --cluster-config cluster_config.jsonIf you also want the browser UI for easier querying, start the web server separately (registry does not launch it automatically):
python -m web.server --port 8080 --registry localhost:50050The registry will verify node stake eligibility on-chain, post settlements, and handle slashing.
Racing still uses first-valid-wins for request serving, but settlement payouts are not winner-only. The registry accepts race-winner reports and signs winner receipts, then applies a winner-share bonus during settlement weighting:
- all validated shares still participate in payout split
- winner nodes receive extra share weight for reported winning hops
- total settlement payout is unchanged (bonus is reweighted within the same pool)
Winner-bonus policy is cluster-config driven:
winner_bonus_per_report(default0.25)winner_bonus_cap_ratio(default0.5)winner_receipt_store_path(default~/.unfed/registry_winner_receipts.jsonl)winner_receipt_store_max_entries(default200000)
Racing clients now report winners in batches per decode step to reduce control-plane RPC
overhead (ReportRaceWinners), with automatic fallback to single-report mode when
connected to older registries.
The static docs are in site/ and include:
- Getting started:
site/getting-started.html - Architecture and protocol flow:
site/architecture.html,site/protocol.html - Economics and staking/slashing model:
site/economics.html - Full config reference by node type:
site/config-reference.html
Community links:
- GitHub: https://github.com/risha-max/UNFED-AI
- Discord: https://discord.gg/7N73crmnbu
The deploy script creates a test UnfedToken, but the escrow contract accepts any ERC-20. To use your own token:
- Deploy
UnfedEscrowwith your token's address (editDeploy.s.solor pass it as an env var) - Set
staking_token_addressincluster_config.jsonto your token's address - Nodes must hold and stake your token to be eligible
- Nodes call
stake()on the escrow contract with the required minimum (default: 100 tokens) - The registry checks
isEligible()on-chain before accepting a node - Clients deposit tokens into escrow; inference fees are deducted per-request
- The operator posts settlement batches via
postSettlement(); after the challenge window,finalizeSettlement()unlocks claimable payouts - Misbehaving nodes can be slashed (50% of stake, configurable)
On a testnet the operator holds all minted tokens. Clients can request free test tokens from the web dashboard:
- Open the dashboard and enter your wallet address in the Settings sidebar.
- Click Get Test Tokens.
- The server calls
depositFor(address, amount)on the escrow contract, which transfers tokens from the operator and credits them directly to the client's escrow balance.
Rate limit: one drip (100 tokens) per address per hour. The amount and cooldown
are configurable via OnChainEscrow.FAUCET_DRIP_AMOUNT and
OnChainEscrow.FAUCET_COOLDOWN.
Faucet is disabled by default and must be explicitly enabled with:
export UNFED_FAUCET_ENABLED=1By default, faucet requests also require a wallet-authenticated session
(UNFED_FAUCET_REQUIRE_AUTH=1). For local test-only automation, you can disable
that check with UNFED_FAUCET_REQUIRE_AUTH=0.
API: POST /api/faucet with body {"address": "0x...", "session_token": "<optional when auth disabled>"}. Returns
{"success": true, "amount": 100, "tx_hash": "0x...", "balance": 200.0} on
success, or an error with the appropriate HTTP status.
Address validation is strict: invalid/non-EVM addresses are rejected with
HTTP 400. Faucet cooldown is keyed on normalized checksum addresses, so case
changes do not bypass rate limits.
Billing is tokenizer-based and model-specific (no character-length heuristics):
- Text requests: input tokens are counted with the selected model tokenizer.
- Vision-language requests: input tokens are counted from the same model processor/tokenizer path used to build multimodal inputs (including image placeholder tokens).
- Output tokens are counted from streamed generation events.
Cost is reported as:
(input_tokens * price_per_input_token) + (output_tokens * price_per_output_token)
Strict mode behavior:
- If model tokenizer/processor cannot be loaded, the request fails (no fallback estimate and no usage report).
- Usage reports with invalid token counts are rejected by the registry.
| Variable | Description | Default |
|---|---|---|
UNFED_REGISTRY |
Registry address | localhost:50050 |
UNFED_SEEDS |
Comma-separated seed registries | from network/seeds.json |
UNFED_WIRE_DTYPE |
Wire format for activations | float16 |
UNFED_COMPRESS_THRESHOLD |
Compression threshold (bytes) | 16384 |
UNFED_PREFILL_MIN |
Min tokens for pipelined prefill | 64 |
UNFED_REQUIRE_MPC |
Require MPC shard-0 for admission | 1 |
UNFED_STAKE_EVM_PRIVATE_KEY |
Staked node wallet key (direct env) | — |
UNFED_STAKE_EVM_PRIVATE_KEY_FILE |
File path containing staked node wallet key | — |
UNFED_AUTH_MAX_SKEW_MS |
Max timestamp skew for signed control/auth payloads | 300000 |
UNFED_AUTH_NONCE_MAX_ENTRIES |
Max nonce cache entries before LRU-style eviction | 200000 |
UNFED_AUTH_RATE_LIMIT_PER_MINUTE |
Per-node/per-peer auth RPC rate cap | 120 |
UNFED_STAKE_REVALIDATE_TTL_SECONDS |
Eligibility cache TTL before on-chain recheck | 30 |
CHAIN_RPC_URL |
Ethereum RPC endpoint | — |
OPERATOR_PRIVATE_KEY |
Operator wallet key (deploy/settlement) | — |
OPERATOR_ADDRESS |
Operator wallet address | — |
UNFED_REQUIRE_MPC=1 is the default fail-closed policy across web, client,
and pipeline entrypoints. Set UNFED_REQUIRE_MPC=0 only for local debugging.
For OPSEC, prefer file-based secret loading for staked node keys:
umask 077
printf '%s' "0xYOUR_STAKED_NODE_PRIVATE_KEY" > ~/.unfed/node_stake.key
export UNFED_STAKE_EVM_PRIVATE_KEY_FILE=~/.unfed/node_stake.keyUse the orchestration script to launch chain/funding/registry/daemon/nodes/web
in separate gnome-terminal windows:
export OPERATOR_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
export OPERATOR_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
./scripts/manual_setup_mpc_testnet.shUseful flags:
--dry-run: print commands without starting processes--yes-reuse-ports: continue even if expected ports are already occupied
- Split or download the model shards
- Pick a shard index (coordinate with other operators or use
--auto) - Open firewall ports
- Start your node with
--advertisepointing to your public IP - Verify registration via the web dashboard or
python -m client.client --list-models
CLI:
python -m client.client \
--registry "registry.unfed.ai:50050" \
--prompt "Explain quantum computing" \
--max-tokens 100Add --tls-ca ca.crt if the network uses TLS.
Web interface: Every registry hosts a web dashboard. Open the registry's HTTP address in a browser to chat, inspect the network, and explore the share-chain — no separate client install needed.
The registry operator starts the dashboard with:
python -m web.server --host 127.0.0.1 --port 8080 --registry "localhost:50050"Open http://<registry-ip>:8080 — tabs for Chat, Network topology, and Chain
explorer. Clients visiting the URL can send inference requests directly from
the browser, get test tokens via the faucet, and monitor node activity.
Model selection is strict and registry-driven:
- The dashboard loads model options from
GET /api/modelsonly (no frontend hardcoded list). - A request is accepted only for a model that passes admission preflight:
- full text shard coverage
- MPC shard-0 entry available (default policy)
- full vision shard coverage for multimodal requests
- If preflight fails, the request is rejected with a model-scoped error.
Security restrictions:
- Websocket
walletand faucetaddressinputs must be valid EVM addresses. cluster_endpointoverrides are blocked by default; only the co-located registry (or entries fromUNFED_ALLOWED_CLUSTER_ENDPOINTS) are accepted.
Troubleshooting:
- No models discovered: ensure compute nodes are registered with the intended
model_idand shard coverage is complete. - Model rejected by policy (MPC missing): register an MPC shard-0 node, or
set
UNFED_REQUIRE_MPC=0for local debugging only. - Model rejected by policy (coverage incomplete): verify all required shards
are registered for that
model_id.
- Persistent identity required in on-chain mode: compute/vision/MPC/daemon should use a staked EVM identity (
--eth-address) and authenticated registration/control-plane signatures - Testnet economics: on-chain escrow works but uses test tokens — no real value at stake
- Single model: the testnet runs one model (Qwen2.5-0.5B); multi-model pools are implemented but untested at scale
- No NAT traversal: nodes behind NAT must configure port forwarding manually
- TLS is optional: without it, traffic is plaintext — do not send sensitive data