RESTORE — Technical Blueprint · v2.1 · March 2026
The implementation guide for developers and node operators. BLE handshakes, Merkle ledgers, P2P inference, governance protocol, circuit breakers. Everything you need to build on, fork, or run your own node.
table of contents
01 —
┌──────────────────────────────────────────────────────────┐
│ USER DEVICE (PWA) │
│──────────────────────────────────────────────────────────│
│ Tier 1: Deterministic State Machine (0–50ms) │
│ Tier 2: P2P Distributed Inference (5–15s) │
│ Tier 3: API Fallback — Claude/OpenAI (optional) │
│ Local: SQLite + IPFS + Encrypted agent memory │
└──────────────────────────────────────────────────────────┘
↕ libp2p ↕ Nostr events ↕ BLE mesh
┌──────────────────────────────────────────────────────────┐
│ P2P NETWORK LAYER │
│──────────────────────────────────────────────────────────│
│ libp2p peer discovery + DHT routing │
│ Nostr relay network (event propagation) │
│ IPFS content storage (photos, agent memory, ledger) │
│ BLE mesh (proximity verification, offline operation) │
└──────────────────────────────────────────────────────────┘
↕ protocol events ↕ ledger sync
┌──────────────────────────────────────────────────────────┐
│ PROTOCOL LAYER │
│──────────────────────────────────────────────────────────│
│ SC Merkle Ledger — append-only, distributed │
│ Anomaly Agents — circuit breakers, fraud detection │
│ Governance Engine — proposal routing, vote tallying │
│ World Agent Registry — location seeds, health scoring │
└──────────────────────────────────────────────────────────┘| Layer | Technology |
|---|---|
| Frontend | Progressive Web App (PWA), TypeScript |
| Local storage | SQLite (via sql.js-httpvfs), IndexedDB |
| Networking | libp2p, WebRTC data channels |
| Event protocol | Nostr (NIP-01 base + custom kinds 1001–1011) |
| Content storage | IPFS / Helia (browser-compatible) |
| Proximity | Web Bluetooth API (BLE) |
| Cryptography | @noble/ed25519, @noble/ciphers/chacha, @noble/hashes |
| Local inference | llama.cpp WASM / Ollama sidecar |
| P2P inference | Petals-compatible node network |
| Bundler | Vite |
| Tests | Vitest, Playwright |
02 —
git clone https://github.com/restore-protocol/app cd app npm install npm run dev # → http://localhost:5173
git clone https://github.com/restore-protocol/relay cd relay npm install cp .env.example .env # edit port and storage path npm start
git clone https://github.com/restore-protocol/petals-node cd petals-node pip install -r requirements.txt python run_node.py --port 8080 --model llama-3-8b-instruct
git clone https://github.com/restore-protocol/ipfs-node cd ipfs-node docker compose up -d
VITE_NOSTR_RELAYS=wss://relay1.restore.xyz,wss://relay2.restore.xyz VITE_IPFS_GATEWAYS=https://ipfs.restore.xyz,https://cloudflare-ipfs.com VITE_ANTHROPIC_API_KEY= # optional Tier 3 fallback VITE_PETALS_BOOTSTRAP= # bootstrap node for P2P inference VITE_NETWORK=mainnet # or testnet
03 —
Every player's identity is a single ed25519 keypair. The public key is the player's permanent pseudonymous identifier across all RESTORE systems. The private key never leaves the device.
import { generateKeyPair, getPublicKey } from '@noble/ed25519';
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
import { sha256 } from '@noble/hashes/sha256';
async function initializeIdentity(passphrase: string): Promise<Identity> {
const privateKey = crypto.getRandomValues(new Uint8Array(32));
const publicKey = await getPublicKey(privateKey);
// Derive encryption key from passphrase
const passphraseBytes = new TextEncoder().encode(passphrase);
const encryptionKey = sha256(passphraseBytes);
const nonce = crypto.getRandomValues(new Uint8Array(24));
const cipher = xchacha20poly1305(encryptionKey, nonce);
const encryptedPrivateKey = cipher.encrypt(privateKey);
await storeSecurely('identity', {
publicKey: bytesToHex(publicKey),
encryptedPrivateKey: bytesToHex(encryptedPrivateKey),
nonce: bytesToHex(nonce),
createdAt: Date.now(),
version: 1
});
return { privateKey, publicKey };
}import { sign, verify } from '@noble/ed25519';
async function signEvent(event: UnsignedEvent, privateKey: Uint8Array): Promise<SignedEvent> {
const serialised = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
]);
const hash = sha256(new TextEncoder().encode(serialised));
const sig = await sign(hash, privateKey);
return {
...event,
id: bytesToHex(hash),
sig: bytesToHex(sig)
};
}
async function verifyEvent(event: SignedEvent): Promise<boolean> {
const serialised = JSON.stringify([
0, event.pubkey, event.created_at, event.kind, event.tags, event.content
]);
const hash = sha256(new TextEncoder().encode(serialised));
return verify(hexToBytes(event.sig), hash, hexToBytes(event.pubkey));
}async function exportWallet(passphrase: string): Promise<string> {
const identity = await getIdentity();
const backup = {
version: '2.1',
publicKey: bytesToHex(identity.publicKey),
encryptedPrivateKey: await encryptKey(identity.privateKey, passphrase),
scBalance: await getSCBalance(),
scLifetimeEarned: await getLifetimeSC(),
accountAge: Math.floor((Date.now() - identity.createdAt) / 86400000),
missionCount: await getMissionCount(),
exportedAt: Date.now()
};
return JSON.stringify(backup, null, 2);
}
async function importWallet(backup: string, passphrase: string): Promise<void> {
const data = JSON.parse(backup);
const privateKey = await decryptKey(data.encryptedPrivateKey, passphrase);
const derived = await getPublicKey(privateKey);
if (bytesToHex(derived) !== data.publicKey) {
throw new Error('Passphrase incorrect or backup corrupted');
}
await storeSecurely('identity', { ...data, privateKey });
}04 —
Physical proximity verification via Bluetooth Low Energy. No GPS spoofing. No AI-generated photos. Two Bluetooth radios must be within ~3 metres for the challenge-response to complete.
const RESTORE_SERVICE_UUID = '0000AAAA-0000-1000-8000-00805f9b34fb'; const CHALLENGE_CHAR_UUID = '0000BBBB-0000-1000-8000-00805f9b34fb'; const RESPONSE_CHAR_UUID = '0000CCCC-0000-1000-8000-00805f9b34fb'; const FINAL_SIG_CHAR_UUID = '0000DDDD-0000-1000-8000-00805f9b34fb';
function generateBLEAdvertisingData(publicKey: Uint8Array, timestamp: number): Uint8Array {
// Time bucket prevents tracking across sessions
const timeBucket = Math.floor(timestamp / (15 * 60 * 1000));
const input = new Uint8Array([...publicKey, ...numberToBytes(timeBucket)]);
return sha256(input).slice(0, 16);
}async function initiateHandshake(missionId: string): Promise<EncounterReceipt> {
const { privateKey, publicKey } = await getIdentity();
const nonceA = crypto.getRandomValues(new Uint8Array(32));
const timestamp = Date.now();
// Discover peer via BLE scan
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [RESTORE_SERVICE_UUID] }],
optionalServices: [RESTORE_SERVICE_UUID]
});
const server = await device.gatt!.connect();
const service = await server.getPrimaryService(RESTORE_SERVICE_UUID);
// Step 1: Send challenge nonce to peer
const challengeChar = await service.getCharacteristic(CHALLENGE_CHAR_UUID);
const challengePayload = new Uint8Array([
...publicKey,
...nonceA,
...new TextEncoder().encode(missionId)
]);
await challengeChar.writeValueWithResponse(challengePayload);
// Step 2: Read peer's response (pubkey + nonceB + signature of nonceA)
const responseChar = await service.getCharacteristic(RESPONSE_CHAR_UUID);
const response = new Uint8Array((await responseChar.readValue()).buffer);
const peerPublicKey = response.slice(0, 32);
const nonceB = response.slice(32, 64);
const peerSigOfA = response.slice(64, 128);
// Step 3: Verify peer signed our nonce
const msgA = new Uint8Array([...nonceA, ...numberToBytes(timestamp), ...new TextEncoder().encode(missionId)]);
const isValid = await verify(peerSigOfA, sha256(msgA), peerPublicKey);
if (!isValid) throw new Error('Peer signature verification failed');
// Step 4: Sign peer's nonce and send back
const msgB = new Uint8Array([...nonceB, ...numberToBytes(timestamp), ...new TextEncoder().encode(missionId)]);
const mySigOfB = await sign(sha256(msgB), privateKey);
const finalChar = await service.getCharacteristic(FINAL_SIG_CHAR_UUID);
await finalChar.writeValueWithResponse(mySigOfB);
// Build and return encounter receipt
const receipt: EncounterReceipt = {
missionId,
timestamp,
peerA: {
publicKey: bytesToHex(publicKey),
nonce: bytesToHex(nonceA),
signatureOfB: bytesToHex(mySigOfB)
},
peerB: {
publicKey: bytesToHex(peerPublicKey),
nonce: bytesToHex(nonceB),
signatureOfA: bytesToHex(peerSigOfA)
}
};
await encounterQueue.enqueue(receipt);
return receipt;
}class EncounterQueue {
private db: IDBDatabase;
async enqueue(receipt: EncounterReceipt): Promise<void> {
await this.db.put('encounters', {
receipt,
queuedAt: Date.now(),
attempts: 0,
status: 'pending'
});
if (navigator.onLine) this.syncAll();
}
async syncAll(): Promise<void> {
const pending = await this.db.getAll('encounters');
const unsent = pending.filter(e => e.status === 'pending');
for (const item of unsent) {
try {
await broadcastToNostr({
kind: 1002,
content: JSON.stringify(item.receipt),
tags: [
['mission', item.receipt.missionId],
['peer', item.receipt.peerB.publicKey]
]
});
item.status = 'synced';
} catch {
item.attempts++;
if (item.attempts >= 10) item.status = 'failed';
}
await this.db.put('encounters', item);
}
}
}05 —
class WebOfTrust {
private graph = new Map<string, TrustNode>();
addHandshake(pubkeyA: string, pubkeyB: string, timestamp: number): void {
// Weight decays with age; handshakes more than 6 months old contribute less
const ageMs = Date.now() - timestamp;
const decayDays = ageMs / (1000 * 60 * 60 * 24);
const weight = Math.max(0.1, 1 - (decayDays / 180));
this.addEdge(pubkeyA, pubkeyB, weight);
this.addEdge(pubkeyB, pubkeyA, weight);
}
calculateTrustScores(iterations: number = 20): void {
const dampingFactor = 0.85;
const nodes = Array.from(this.graph.values());
nodes.forEach(n => n.trustScore = 1.0);
for (let i = 0; i < iterations; i++) {
const newScores = new Map<string, number>();
for (const node of nodes) {
let score = 1 - dampingFactor;
for (const edge of node.incomingEdges) {
const source = this.graph.get(edge.sourcePubkey);
if (!source) continue;
const outTotal = source.outgoingEdges.reduce((s, e) => s + e.weight, 0);
if (outTotal > 0) {
score += dampingFactor * (source.trustScore * edge.weight) / outTotal;
}
}
newScores.set(node.publicKey, score);
}
newScores.forEach((score, pk) => {
const node = this.graph.get(pk);
if (node) node.trustScore = score;
});
}
}
// Detect isolated clusters of accounts that only verify each other
detectSybilClusters(minClusterSize: number = 5, maxExternalTrust: number = 10): string[][] {
const visited = new Set<string>();
const sybilClusters: string[][] = [];
for (const [pk] of this.graph) {
if (visited.has(pk)) continue;
const cluster = this.bfsCluster(pk, visited);
if (cluster.length < minClusterSize) continue;
const maxExternalScore = Math.max(...cluster.map(pk => {
const node = this.graph.get(pk)!;
return node.incomingEdges
.filter(e => !cluster.includes(e.sourcePubkey))
.reduce((s, e) => {
const src = this.graph.get(e.sourcePubkey);
return Math.max(s, src?.trustScore ?? 0);
}, 0);
}));
if (maxExternalScore < maxExternalTrust) {
sybilClusters.push(cluster);
}
}
return sybilClusters;
}
getVotingWeight(pubkey: string, scLifetime: number, accountAgeDays: number): number {
const trustScore = this.graph.get(pubkey)?.trustScore ?? 0;
const popMultiplier = Math.min(2.0, Math.max(0.5, trustScore / 50));
const ageMultiplier = accountAgeDays < 180 ? 1.0
: accountAgeDays < 730 ? 1.0 + ((accountAgeDays - 180) / 550) * 0.5
: 1.5;
return Math.sqrt(scLifetime) * ageMultiplier * popMultiplier;
}
}const VELOCITY_LIMITS = {
maxHandshakesPerDay: 15,
samePeerCooldownDays: 30,
maxSCPerHourPerHex: 1000, // circuit breaker tripwire
maxMissionsPerDayPerPlayer: 20
};
async function checkHandshakeVelocity(playerPubkey: string, peerPubkey: string): Promise<boolean> {
const today = new Date().toDateString();
const dailyCount = await db.count('handshakes', { player: playerPubkey, date: today });
if (dailyCount >= VELOCITY_LIMITS.maxHandshakesPerDay) return false;
const cooloffMs = VELOCITY_LIMITS.samePeerCooldownDays * 24 * 60 * 60 * 1000;
const lastHandshake = await db.getLast('handshakes', { player: playerPubkey, peer: peerPubkey });
if (lastHandshake && (Date.now() - lastHandshake.timestamp) < cooloffMs) return false;
return true;
}06 —
import { createLibp2p } from 'libp2p';
import { webRTC } from '@libp2p/webrtc';
import { webSockets } from '@libp2p/websockets';
import { noise } from '@chainsafe/libp2p-noise';
import { yamux } from '@chainsafe/libp2p-yamux';
import { kadDHT } from '@libp2p/kad-dht';
import { gossipsub } from '@chainsafe/libp2p-gossipsub';
const node = await createLibp2p({
transports: [webRTC(), webSockets()],
connectionEncryption: [noise()],
streamMuxers: [yamux()],
services: {
dht: kadDHT({ protocol: '/restore/kad/1.0.0' }),
pubsub: gossipsub({
emitSelf: false,
allowPublishToZeroTopicPeers: true,
heartbeatInterval: 1000
})
}
});
// Topic channels
const TOPICS = {
missions: '/restore/missions/1.0.0',
sc: '/restore/sc/1.0.0',
governance: '/restore/governance/1.0.0',
worldAgents: '/restore/worldagents/1.0.0',
anomalies: '/restore/anomalies/1.0.0'
};import { createHelia } from 'helia';
import { unixfs } from '@helia/unixfs';
const helia = await createHelia();
const fs = unixfs(helia);
async function storeVerificationPhoto(file: File): Promise<string> {
const buffer = await file.arrayBuffer();
const cid = await fs.addBytes(new Uint8Array(buffer));
return cid.toString(); // Content-addressed — same content = same CID
}
async function retrievePhoto(cid: string): Promise<Uint8Array> {
const chunks: Uint8Array[] = [];
for await (const chunk of fs.cat(CID.parse(cid))) {
chunks.push(chunk);
}
return concat(chunks);
}
// Pin high-value verification records redundantly
async function pinCritical(cid: string): Promise<void> {
await helia.pins.add(CID.parse(cid));
}07 —
Every SC transaction is cryptographically chained. Any node can verify the entire history from genesis to now without trusting any other node. No SC can be created, altered, or deleted without the network detecting it immediately.
import { sha256 } from '@noble/hashes/sha256';
interface SCTransaction {
id: string;
previousHash: string; // Hash of the prior transaction
timestamp: number;
from: string | null; // null for creation events
to: string; // recipient public key
amount: number;
missionId: string;
encounterReceiptCid: string; // IPFS CID of the BLE receipt
status: 'VALID' | 'DISPUTED' | 'ORPHANED';
signature: string;
}
function computeTransactionHash(tx: Omit<SCTransaction, 'id'>): string {
const data = JSON.stringify({
previousHash: tx.previousHash,
timestamp: tx.timestamp,
from: tx.from,
to: tx.to,
amount: tx.amount,
missionId: tx.missionId,
encounterReceiptCid: tx.encounterReceiptCid
});
return bytesToHex(sha256(new TextEncoder().encode(data)));
}
async function appendTransaction(
tx: Omit<SCTransaction, 'id' | 'previousHash' | 'signature'>,
privateKey: Uint8Array
): Promise<SCTransaction> {
const lastTx = await ledger.getLatest();
const previousHash = lastTx ? lastTx.id : '0'.repeat(64);
const withHash: Omit<SCTransaction, 'id' | 'signature'> = { ...tx, previousHash };
const id = computeTransactionHash(withHash);
const signature = bytesToHex(await sign(hexToBytes(id), privateKey));
const finalTx: SCTransaction = { ...withHash, id, signature };
await ledger.append(finalTx);
await broadcastSCTransaction(finalTx);
return finalTx;
}
// Verify ledger integrity from genesis
async function verifyLedgerIntegrity(): Promise<{ valid: boolean; failedAt?: string }> {
const transactions = await ledger.getAll();
for (let i = 1; i < transactions.length; i++) {
const tx = transactions[i];
const prevTx = transactions[i - 1];
// Verify chain linkage
if (tx.previousHash !== prevTx.id) {
return { valid: false, failedAt: tx.id };
}
// Verify transaction hash
const { id, signature, ...rest } = tx;
const expectedId = computeTransactionHash(rest);
if (expectedId !== id) {
return { valid: false, failedAt: tx.id };
}
// Verify signature
const sigValid = await verify(hexToBytes(signature), hexToBytes(id), hexToBytes(tx.to));
if (!sigValid) {
return { valid: false, failedAt: tx.id };
}
}
return { valid: true };
}08 —
interface CommunityCapacity {
communityId: string;
verifiedCapacities: {
energyKWhPerDay: number;
labourHoursPerWeek: number;
foodKgPerWeek: number;
};
timeHorizonDays: number;
trustScore: number; // 0.5 (new) to 2.0 (established)
}
const SC_RATES = {
energyPerKWh: 2, // SC per kWh
labourPerHour: 25, // SC per hour
foodPerKg: 5 // SC per kg
};
function calculateCommunityMaxCredit(capacity: CommunityCapacity): number {
const { energyKWhPerDay, labourHoursPerWeek, foodKgPerWeek } = capacity.verifiedCapacities;
const dailyEnergySC = energyKWhPerDay * SC_RATES.energyPerKWh;
const weeklyLabourSC = labourHoursPerWeek * SC_RATES.labourPerHour;
const weeklyFoodSC = foodKgPerWeek * SC_RATES.foodPerKg;
// Convert all to monthly equivalent
const monthlySC = (dailyEnergySC * 30) + (weeklyLabourSC * 4) + (weeklyFoodSC * 4);
const timeMultiplier = capacity.timeHorizonDays / 30;
return Math.floor(monthlySC * timeMultiplier * capacity.trustScore);
}
function calculatePlayerCreditLimit(profile: {
avgDailyContributionSC: number;
communityTrustWeight: number; // Web of Trust score 0.5–2.0
skillMultipliers: Record<string, number>;
}): number {
const baseLimit = profile.avgDailyContributionSC * 30;
const trustAdjusted = baseLimit * profile.communityTrustWeight;
const skillBonus = Object.values(profile.skillMultipliers).reduce((s, m) => s + (m - 1), 0);
return Math.floor(trustAdjusted * (1 + skillBonus));
}interface TrustCertificate {
version: string;
playerKey: string;
originCommunity: string;
scLifetimeBalance: number;
missionCategories: string[];
accountAgeDays: number;
handshakeCount: number;
defaultHistory: DefaultRecord[];
issuedAt: number;
cryptographicProof: string;
}
async function exportTrustCertificate(playerKey: string): Promise<TrustCertificate> {
const player = await db.getPlayer(playerKey);
const certificate: Omit<TrustCertificate, 'cryptographicProof'> = {
version: '2.1',
playerKey,
originCommunity: player.communityId,
scLifetimeBalance: player.lifetimeEarned,
missionCategories: await db.getMissionCategories(playerKey),
accountAgeDays: Math.floor((Date.now() - player.createdAt) / 86400000),
handshakeCount: await db.getHandshakeCount(playerKey),
defaultHistory: await db.getDefaults(playerKey),
issuedAt: Date.now()
};
const certBytes = new TextEncoder().encode(JSON.stringify(certificate));
const privateKey = await getPrivateKey();
const proof = await sign(sha256(certBytes), privateKey);
return { ...certificate, cryptographicProof: bytesToHex(proof) };
}
async function verifyCertificate(cert: TrustCertificate): Promise<boolean> {
const { cryptographicProof, ...rest } = cert;
const certBytes = new TextEncoder().encode(JSON.stringify(rest));
return verify(hexToBytes(cryptographicProof), sha256(certBytes), hexToBytes(cert.playerKey));
}
// Calculate provisional credit limit for a new arrival
function calculateProvisionallimit(cert: TrustCertificate): number {
if (cert.defaultHistory.length > 0) return 100; // Very restricted
const ratio = cert.accountAgeDays > 365 ? 0.75 : 0.5;
return Math.floor(calculateCommunityMaxCredit({
communityId: 'new',
verifiedCapacities: { energyKWhPerDay: 0, labourHoursPerWeek: 0, foodKgPerWeek: 0 },
timeHorizonDays: 30,
trustScore: 0.5
}) * ratio + cert.scLifetimeBalance * 0.01);
}// Communities can vote to enable demurrage — negative interest on idle SC
async function applyDemurrage(playerKey: string, ratePerDay: number = 0.001): Promise<void> {
const balance = await db.getSCBalance(playerKey);
const lastActivity = await db.getLastActivityDate(playerKey);
const idleDays = Math.floor((Date.now() - lastActivity) / 86400000);
if (idleDays < 30) return; // Grace period
const decay = balance * (1 - Math.pow(1 - ratePerDay, idleDays - 30));
const adjusted = Math.floor(balance - decay);
await db.setSCBalance(playerKey, adjusted);
await broadcastSCTransaction({
from: playerKey,
to: 'demurrage-pool',
amount: balance - adjusted,
missionId: 'demurrage',
encounterReceiptCid: '',
timestamp: Date.now(),
status: 'VALID'
});
}09 —
const ANOMALY_TRIPWIRES = {
velocityMultiplier: 5.0, // 500% above baseline triggers freeze
concentrationMax: 0.3, // Single node receiving >30% of regional SC
patternDeviation: 3.0, // Mission category >300% of historical baseline
windowMs: 60 * 60 * 1000, // 1-hour rolling window
hexResolution: 10, // H3 hex resolution (10km²)
baselinePeriodDays: 30 // Days of history for baseline calculation
};class AnomalyDetectionEngine {
private monitoring = true;
async startMonitoring(): Promise<void> {
setInterval(async () => {
if (!this.monitoring) return;
await this.checkVelocityAnomalies();
await this.checkConcentrationAnomalies();
await this.checkPatternAnomalies();
}, 5000); // Check every 5 seconds
}
private async checkVelocityAnomalies(): Promise<void> {
const hexes = await db.getActiveHexes();
for (const hexId of hexes) {
const currentHour = await this.getSCVelocity(hexId, ANOMALY_TRIPWIRES.windowMs);
const baseline = await this.getBaselineVelocity(hexId, ANOMALY_TRIPWIRES.baselinePeriodDays);
if (baseline > 0 && currentHour / baseline > ANOMALY_TRIPWIRES.velocityMultiplier) {
await this.triggerCircuitBreaker({
type: 'VELOCITY',
target: hexId,
currentValue: currentHour,
baselineValue: baseline,
ratio: currentHour / baseline
});
}
}
}
async triggerCircuitBreaker(violation: Violation): Promise<void> {
const freezeEvent: FreezeEvent = {
id: generateUUID(),
type: violation.type,
target: violation.target,
triggeredAt: Date.now(),
evidence: {
currentValue: violation.currentValue,
baselineValue: violation.baselineValue,
ratio: violation.ratio
},
status: 'ACTIVE'
};
// Broadcast freeze to all nodes
await broadcastNostrEvent({
kind: 1011,
content: JSON.stringify(freezeEvent),
tags: [['target', violation.target], ['ratio', String(violation.ratio.toFixed(1))]]
});
// Halt new SC validation from this source
await this.freezeSCGeneration(violation.target);
// Mark recent SC from this source as DISPUTED
await this.markRecentSCDisputed(violation.target, ANOMALY_TRIPWIRES.windowMs);
// Auto-draft emergency governance proposal
await this.draftEmergencyProposal(freezeEvent);
}
private async draftEmergencyProposal(freeze: FreezeEvent): Promise<void> {
const evidence = await this.gatherEvidence(freeze.target, freeze.triggeredAt);
const proposal: GovernanceProposal = {
id: generateUUID(),
type: 'EMERGENCY',
title: `Circuit Breaker: ${freeze.type} — ${freeze.target}`,
description: `Auto-generated by anomaly detection agent.\n\n` +
`Source: ${freeze.target}\n` +
`Deviation: ${freeze.evidence.ratio.toFixed(0)}× baseline\n` +
`SC at risk: ${evidence.disputedSC}\n\n` +
`Community must vote within 72 hours:\n` +
`- CLEAR (legitimate activity confirmed)\n` +
`- ROLLBACK_AND_PATCH (exploit confirmed)`,
options: ['CLEAR', 'ROLLBACK_AND_PATCH'],
votingDeadline: Date.now() + (72 * 60 * 60 * 1000),
quorumRequired: 0.25,
status: 'ACTIVE',
createdBy: 'anomaly-agent',
votes: []
};
await broadcastNostrEvent({
kind: 1004,
content: JSON.stringify(proposal),
tags: [['type', 'EMERGENCY'], ['freeze', freeze.id]]
});
}
}10 —
interface ResponseTemplate {
trigger: RegExp | string;
conditions?: Record<string, { min?: number; max?: number }>;
responses: string[];
sideEffects?: StateUpdate[];
escalate?: boolean;
}
class DeterministicAgent {
private templates: ResponseTemplate[];
private agentState: AgentState;
async processInput(input: string): Promise<AgentResponse> {
const t0 = performance.now();
for (const template of this.templates) {
const matches = typeof template.trigger === 'string'
? input.toLowerCase().includes(template.trigger)
: template.trigger.test(input);
if (!matches) continue;
if (template.conditions && !this.conditionsMet(template.conditions)) continue;
if (template.sideEffects) {
for (const update of template.sideEffects) {
await this.applyStateUpdate(update);
}
}
const response = template.responses[Math.floor(Math.random() * template.responses.length)];
const interpolated = this.interpolate(response, this.agentState);
return {
text: interpolated,
latencyMs: performance.now() - t0,
tier: 1,
escalated: false
};
}
// No template matched — escalate to P2P inference
return this.escalateToLLM(input);
}
private async escalateToLLM(input: string): Promise<AgentResponse> {
const context = this.buildMinimalContext(); // Only what LLM needs
return inferenceRouter.route({
agentType: this.agentType,
input,
context,
maxTokens: 200 // Token starvation — keeps responses tight and fast
});
}
}class AgentMemory {
private encryptionKey: Uint8Array;
async store(key: string, value: unknown): Promise<void> {
const serialised = JSON.stringify(value);
const nonce = crypto.getRandomValues(new Uint8Array(24));
const cipher = xchacha20poly1305(this.encryptionKey, nonce);
const encrypted = cipher.encrypt(new TextEncoder().encode(serialised));
await db.put('agent_memory', {
key,
nonce: bytesToHex(nonce),
data: bytesToHex(encrypted),
updatedAt: Date.now()
});
}
async retrieve<T>(key: string): Promise<T | null> {
const record = await db.get('agent_memory', key);
if (!record) return null;
const cipher = xchacha20poly1305(this.encryptionKey, hexToBytes(record.nonce));
const decrypted = cipher.decrypt(hexToBytes(record.data));
return JSON.parse(new TextDecoder().decode(decrypted)) as T;
}
// Export memory to IPFS for cross-device sync (player-controlled)
async exportToIPFS(): Promise<string> {
const allMemory = await db.getAll('agent_memory');
const cid = await ipfs.storeEncrypted(allMemory, this.encryptionKey);
return cid;
}
}interface MissionSpec {
type: 'body' | 'mind' | 'freedom' | 'purpose' | 'tribe' | 'energy';
difficulty: 1 | 2 | 3 | 4 | 5;
scReward: number;
verificationRequirements: ('ble' | 'photo' | 'gps' | 'peer_attestation')[];
title: string;
description: string;
actions: string[];
}
// LLM generates minimal structured output — app inflates into full mission
async function generateMission(agentType: string, playerContext: MinimalContext): Promise<MissionSpec> {
const prompt = buildStructuredPrompt(agentType, playerContext);
const raw = await inferenceRouter.route({
input: prompt,
maxTokens: 150,
systemPrompt: 'Respond ONLY with valid JSON matching MissionSpec. No preamble. No explanation.'
});
return JSON.parse(raw.text) as MissionSpec;
}11 —
class InferenceRouter {
async route(request: InferenceRequest): Promise<InferenceResponse> {
// Tier 1: Deterministic (already handled in agent, but fallback here)
if (request.complexity === 'low') {
return this.tier1Local(request);
}
// Tier 2: P2P network (Petals-style)
try {
const p2pResponse = await Promise.race([
this.tier2P2P(request),
this.timeout(15000) // 15s max wait
]);
return p2pResponse as InferenceResponse;
} catch {
// Tier 3: API fallback
if (this.apiKeyAvailable()) {
return this.tier3API(request);
}
return this.tier1Fallback(request); // Degrade gracefully
}
}
private async tier2P2P(request: InferenceRequest): Promise<InferenceResponse> {
const nodes = await p2pNetwork.findInferenceNodes(request.modelSize);
if (nodes.length < 3) throw new Error('Insufficient P2P nodes');
const shards = this.shardRequest(request, nodes.length);
const results = await Promise.all(
shards.map((shard, i) => nodes[i].process(shard))
);
return this.assembleShards(results);
}
private async tier3API(request: InferenceRequest): Promise<InferenceResponse> {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: request.maxTokens ?? 200,
system: request.systemPrompt ?? '',
messages: [{ role: 'user', content: request.input }]
})
});
const data = await response.json();
return { text: data.content[0].text, tier: 3, latencyMs: 0 };
}
}// Force minimal output — app expands with pre-written narrative templates
const STRUCTURED_PROMPTS = {
generateMission: (ctx: string) =>
`Context: ${ctx}\n\nRespond ONLY with JSON: {"type":"<agent>","difficulty":<1-5>,"reward":<sc>,"title":"<short>","actions":["<action1>","<action2>"]}\nNo other text.`,
assessHealth: (metrics: string) =>
`Metrics: ${metrics}\n\nRespond ONLY with JSON: {"score":<0-100>,"trend":"<up|down|stable>","priority":"<area>"}\nNo other text.`
};12 —
interface GovernanceProposal {
id: string;
type: 'STANDARD' | 'EMERGENCY' | 'CONSTITUTIONAL';
title: string;
description: string;
options: string[];
deliberationDeadline: number; // Deliberation period before voting opens
votingDeadline: number;
quorumRequired: number; // Fraction of eligible voters
supermajorityRequired?: number; // For CONSTITUTIONAL
status: 'DELIBERATION' | 'VOTING' | 'COMPLETE' | 'REJECTED' | 'EXECUTED';
createdBy: string;
createdAt: number;
votes: Vote[];
result?: string;
}
interface Vote {
voterKey: string;
option: string;
weight: number; // sqrt(lifetime_sc) × age_mult × pop_mult
timestamp: number;
signature: string;
}
const PROPOSAL_PARAMS: Record<string, ProposalParams> = {
STANDARD: { deliberationDays: 0, votingDays: 14, quorum: 0.15, supermajority: null },
EMERGENCY: { deliberationDays: 0, votingDays: 3, quorum: 0.25, supermajority: null },
CONSTITUTIONAL: { deliberationDays: 14, votingDays: 30, quorum: 0.40, supermajority: 0.60 }
};
async function castVote(proposalId: string, option: string, privateKey: Uint8Array): Promise<void> {
const proposal = await db.getProposal(proposalId);
if (proposal.status !== 'VOTING') throw new Error('Proposal not in voting phase');
if (Date.now() > proposal.votingDeadline) throw new Error('Voting period has ended');
const pubkey = bytesToHex(await getPublicKey(privateKey));
const existing = proposal.votes.find(v => v.voterKey === pubkey);
if (existing) throw new Error('Already voted');
const player = await db.getPlayer(pubkey);
const weight = webOfTrust.getVotingWeight(pubkey, player.lifetimeEarned, player.accountAgeDays);
const voteData = JSON.stringify({ proposalId, option, weight, timestamp: Date.now() });
const signature = bytesToHex(await sign(sha256(new TextEncoder().encode(voteData)), privateKey));
const vote: Vote = { voterKey: pubkey, option, weight, timestamp: Date.now(), signature };
await broadcastNostrEvent({
kind: 1004,
content: JSON.stringify(vote),
tags: [['proposal', proposalId], ['option', option]]
});
}
async function tallyProposal(proposalId: string): Promise<ProposalResult> {
const proposal = await db.getProposal(proposalId);
const params = PROPOSAL_PARAMS[proposal.type];
const totalEligible = await db.getEligibleVoterCount();
const totalWeightCast = proposal.votes.reduce((s, v) => s + v.weight, 0);
const quorumMet = (proposal.votes.length / totalEligible) >= params.quorum;
const tally: Record<string, number> = {};
for (const vote of proposal.votes) {
tally[vote.option] = (tally[vote.option] ?? 0) + vote.weight;
}
const winner = Object.entries(tally).sort((a, b) => b[1] - a[1])[0];
const winnerFraction = winner[1] / totalWeightCast;
const passed = quorumMet && (
params.supermajority ? winnerFraction >= params.supermajority : winnerFraction > 0.5
);
return { winner: winner[0], passed, quorumMet, winnerFraction, tally };
}13 —
interface WorldAgent {
id: string; // e.g. "KLK-47B"
location: {
lat: number;
lng: number;
h3HexId: string; // H3 cell index at resolution 7
placeName: string;
};
health: {
stability: number; // 0–100: how well problems are being addressed
flourishing: number; // 0–100: growth beyond current baseline
};
personality: string; // LLM-generated from location open data
activeMissions: Mission[];
resourcePool: Resource[];
seededBy: string; // Pubkey of Steward who seeded the agent
createdAt: number;
lastActiveAt: number;
}
// Health scoring — dual axis
function scoreWorldAgent(agent: WorldAgent, recentActivity: ActivitySummary): HealthScore {
const stabilityFactors = [
recentActivity.infrastructureMissionsCompleted / 10,
recentActivity.repairMissionsCompleted / 5,
recentActivity.energyMissionsCompleted / 8
];
const flourishingFactors = [
recentActivity.uniquePlayersActive / 20,
recentActivity.skillShareMissionsCompleted / 5,
recentActivity.tribeMissionsCompleted / 10,
recentActivity.newPlayersOnboarded / 3
];
return {
stability: Math.min(100, average(stabilityFactors) * 100),
flourishing: Math.min(100, average(flourishingFactors) * 100)
};
}async function nominateWorldAgent(
location: { lat: number; lng: number; placeName: string },
nominatorKey: string
): Promise<void> {
const nominator = await db.getPlayer(nominatorKey);
if (nominator.lifetimeEarned < 5000) {
throw new Error('Steward tier (5,000+ lifetime SC) required to nominate World Agents');
}
const personality = await generateAgentPersonality(location);
const proposal: GovernanceProposal = {
id: generateUUID(),
type: 'STANDARD',
title: `Seed World Agent: ${location.placeName}`,
description: `Nomination to seed a new World Agent at ${location.placeName}.\n\nGenerated personality: ${personality}`,
options: ['APPROVE', 'REJECT'],
...computeTimeline('STANDARD'),
status: 'VOTING',
createdBy: nominatorKey,
createdAt: Date.now(),
votes: []
};
await broadcastNostrEvent({ kind: 1004, content: JSON.stringify(proposal) });
}14 —
// SC Transaction (see Section 7 for full implementation)
interface SCTransaction {
id: string;
previousHash: string;
timestamp: number;
from: string | null;
to: string;
amount: number;
missionId: string;
encounterReceiptCid: string;
status: 'VALID' | 'DISPUTED' | 'ORPHANED';
signature: string;
}
// Mission
interface Mission {
id: string;
agentType: 'body' | 'mind' | 'freedom' | 'purpose' | 'tribe' | 'energy';
worldAgentId?: string;
playerId: string;
title: string;
description: string;
actions: string[];
difficulty: 1 | 2 | 3 | 4 | 5;
scReward: number;
verificationRequirements: ('ble' | 'photo' | 'gps' | 'peer_attestation')[];
status: 'ACTIVE' | 'PENDING_VERIFICATION' | 'COMPLETE' | 'EXPIRED';
createdAt: number;
completedAt?: number;
verificationCids: string[]; // IPFS CIDs of verification photos
encounterReceiptId?: string; // BLE receipt if peer verification used
}
// Freeze Event
interface FreezeEvent {
id: string;
type: 'VELOCITY' | 'CONCENTRATION' | 'PATTERN';
target: string;
triggeredAt: number;
evidence: { currentValue: number; baselineValue: number; ratio: number; };
status: 'ACTIVE' | 'CLEARED' | 'ROLLED_BACK';
resolvedAt?: number;
resolutionProposalId?: string;
}
// Player Profile
interface PlayerProfile {
publicKey: string;
scBalance: number;
scLifetimeEarned: number;
accountCreatedAt: number;
communityId: string;
missionStats: Record<string, number>;
handshakeCount: number;
trustScore: number;
tier: 'newcomer' | 'member' | 'contributor' | 'steward'; // steward = 5000+ lifetime SC
defaultHistory: DefaultRecord[];
}15 —
RESTORE uses Nostr's event architecture for propagation: simple, cryptographically sound, censorship-resistant, requiring no central authority.
| Kind | Description | Tags |
|---|---|---|
| `1001` | Mission completion | `['mission', id]`, `['agent', type]`, `['sc', amount]` |
| `1002` | BLE handshake verification | `['mission', id]`, `['peer', pubkey]` |
| `1003` | SC transaction | `['to', pubkey]`, `['amount', sc]`, `['mission', id]` |
| `1004` | Governance vote or proposal | `['proposal', id]`, `['type', type]` |
| `1005` | World Agent state update | `['agent', id]`, `['hex', h3id]` |
| `1006` | Player profile update | `['tier', tier]` |
| `1010` | Default notice | `['player', pubkey]`, `['amount', sc]` |
| `1011` | Circuit breaker freeze | `['target', id]`, `['ratio', float]` |
{
"id": "sha256(serialised_content)",
"pubkey": "ed25519_public_key_hex",
"created_at": 1711234567,
"kind": 1001,
"tags": [
["mission", "mission-uuid"],
["agent", "body"],
["sc", "150"],
["world", "KLK-47B"]
],
"content": "{encrypted_or_plain_json_payload}",
"sig": "ed25519_signature_of_id"
}class RelayPool {
private relays: Map<string, RelayConnection> = new Map();
private readonly minRelays = 3;
async broadcast(event: SignedEvent): Promise<void> {
const connected = Array.from(this.relays.values())
.filter(r => r.status === 'connected');
if (connected.length < this.minRelays) {
await this.connectAdditionalRelays();
}
// Broadcast to all connected relays simultaneously
await Promise.allSettled(connected.map(r => r.send(event)));
}
// Fall back to direct peer routing if all relays hostile
async broadcastDirectPeer(event: SignedEvent): Promise<void> {
const peers = await p2pNetwork.getConnectedPeers();
for (const peer of peers.slice(0, 10)) {
await peer.sendEvent(event);
}
}
}16 —
When the app needs to make a heavy P2P call, it generates a physical prerequisite. By the time the player completes the action, the network has returned the result.
class ImmersionEngine {
private pendingCompute = new Map<string, Promise<unknown>>();
async handleHeavyCompute<T>(task: ComputeTask): Promise<T> {
// Kick off P2P inference immediately
const computePromise = inferenceRouter.route(task);
if (task.requiresPhysicalAction) {
const physicalTask = this.selectPhysicalBuffer(task);
// Show player the physical task while compute runs
await ui.showInstruction(physicalTask);
await this.waitForPhysicalCompletion(physicalTask);
// By now, compute should be done (or nearly done)
} else {
await ui.showProgressNarrative(task.type);
}
return await computePromise as T;
}
private selectPhysicalBuffer(task: ComputeTask): PhysicalAction {
const buffers: Record<string, PhysicalAction> = {
'GENERATE_MISSION': { instruction: 'Walk to the location to scan the area', estimatedMs: 10000 },
'ENERGY_AUDIT': { instruction: 'Take a baseline photo of the electrical panel', estimatedMs: 8000 },
'WORLD_AGENT_LORE': { instruction: 'Walk the perimeter of the location', estimatedMs: 12000 },
'TRIBE_INTRODUCTION': { instruction: 'Introduce yourself to your neighbour', estimatedMs: 15000 }
};
return buffers[task.type] ?? { instruction: 'Preparing...', estimatedMs: 8000 };
}
}// Structured prompts force minimal JSON output
// App inflates the JSON into full narrative using pre-written templates
const NARRATIVE_TEMPLATES = {
mission_repair: (m: MissionSpec) =>
`**${m.title}**\n\n` +
`${m.actions.map((a, i) => `${i + 1}. ${a}`).join('\n')}\n\n` +
`Reward: **${m.scReward} SC** | Difficulty: ${'●'.repeat(m.difficulty)}${'○'.repeat(5 - m.difficulty)}`,
agent_response: (agentType: string, key: string, vars: Record<string, string>) =>
AGENT_RESPONSE_BANK[agentType][key].replace(
/\{\{(\w+)\}\}/g,
(_, k) => vars[k] ?? `{{${k}}}`
)
};17 —
const CACHE_NAME = 'restore-v2.1';
const STATIC_ASSETS = ['/', '/app.js', '/styles.css', '/models/body-agent-micro.gguf'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener('fetch', (event) => {
// Network-first for API calls; cache-first for static assets
if (event.request.url.includes('/api/') || event.request.url.includes('nostr')) {
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
);
} else {
event.respondWith(
caches.match(event.request).then(cached => cached || fetch(event.request))
);
}
});
// Background sync for offline encounters and transactions
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-encounters') event.waitUntil(syncEncounters());
if (event.tag === 'sync-transactions') event.waitUntil(syncTransactions());
if (event.tag === 'sync-votes') event.waitUntil(syncVotes());
});// Every critical action is queued locally first, synced when online
class OfflineQueue {
async enqueue(action: PendingAction): Promise<void> {
await db.put('pending_actions', {
...action,
id: generateUUID(),
queuedAt: Date.now(),
attempts: 0,
status: 'pending'
});
if (navigator.onLine) {
navigator.serviceWorker.ready.then(sw =>
sw.sync.register(`sync-${action.type}`)
);
}
}
}
// Register for push notifications when back online
self.addEventListener('online', async () => {
await syncEncounters();
await syncTransactions();
await syncVotes();
});18 —
| Attack Vector | Mechanism | Mitigation | Status |
|---|---|---|---|
| GPS spoofing | Fake location data | BLE physical proximity replaces GPS as primary verification | ✅ Solved |
| AI-generated verification photos | Fake photo evidence | BLE challenge-response cannot be replicated without physical hardware | ✅ Solved |
| Sybil attack (fake accounts) | Many fake accounts voting or earning SC | Web of Trust + velocity limits + physical handshake requirement | ✅ Mitigated |
| Exploit farm / SC farming | Rapid SC generation from exploited mission | Circuit breakers detect and freeze within 1 hour | ✅ Solved |
| 51% governance attack | Concentrated voting power | Quadratic voting (sqrt) + Proof of Personhood multiplier prevents whale dominance | ✅ Mitigated |
| Supply chain attack | Compromised npm package | Dependency pinning, SLSA provenance, reproducible builds | 🔄 In Progress |
| Private key theft | Stolen device | Keys encrypted at rest; passphrase required to decrypt | ✅ Solved |
| Relay censorship | Hostile relay dropping events | Multi-relay broadcast + direct peer fallback | ✅ Solved |
| P2P inference poisoning | Malicious inference node | Output validation, structured outputs only, results cross-checked | 🔄 In Progress |
// All cryptographic operations use audited @noble/* libraries
import { sign, verify, getPublicKey } from '@noble/ed25519'; // Signatures
import { xchacha20poly1305 } from '@noble/ciphers/chacha'; // AEAD encryption
import { sha256 } from '@noble/hashes/sha256'; // Hashing
import { hkdf } from '@noble/hashes/hkdf'; // Key derivation
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; // Encoding
// No Web Crypto API for signing — @noble/ed25519 is constant-time and audited
// Web Crypto used only for random number generation via crypto.getRandomValues()19 —
describe('BLE Handshake Protocol', () => {
it('completes mutual challenge-response between two players', async () => {
const [alice, bob] = await Promise.all([generateTestIdentity(), generateTestIdentity()]);
const receipt = await simulateBLEHandshake(alice, bob, 'mission-001');
expect(receipt.peerA.publicKey).toBe(bytesToHex(alice.publicKey));
expect(receipt.peerB.publicKey).toBe(bytesToHex(bob.publicKey));
expect(await verifyEncounterReceipt(receipt)).toBe(true);
});
it('rejects tampered challenge nonce', async () => {
const [alice, bob] = await Promise.all([generateTestIdentity(), generateTestIdentity()]);
await expect(simulateBLEHandshake(alice, bob, 'mission-001', { tamperedNonce: true }))
.rejects.toThrow('Peer signature verification failed');
});
it('enforces velocity limits', async () => {
const alice = await generateTestIdentity();
const bobs = await Promise.all(Array.from({ length: 20 }, generateTestIdentity));
// First 15 should succeed
for (let i = 0; i < 15; i++) {
await expect(checkHandshakeVelocity(alice.publicKey, bobs[i].publicKey)).resolves.toBe(true);
}
// 16th should fail
await expect(checkHandshakeVelocity(alice.publicKey, bobs[15].publicKey)).resolves.toBe(false);
});
});describe('Circuit Breakers', () => {
it('triggers on 500% velocity spike', async () => {
const hexId = 'TEST-HEX-001';
await setBaselineSCVelocity(hexId, 100); // SC/hour baseline
// Simulate 6× spike
await generateSCTransactions(hexId, 600, 60 * 60 * 1000);
const freezeEvents = await db.getFreezeEvents(hexId);
expect(freezeEvents).toHaveLength(1);
expect(freezeEvents[0].type).toBe('VELOCITY');
expect(freezeEvents[0].evidence.ratio).toBeGreaterThan(5);
});
it('marks disputed SC as unspendable during freeze', async () => {
const player = await createTestPlayer({ sc: 500 });
await freezePlayer(player.publicKey);
await expect(spendSC(player.publicKey, 100, 'test-mission'))
.rejects.toThrow('SC from disputed source cannot be spent');
});
it('clears disputed SC after CLEAR vote', async () => {
const player = await createTestPlayer({ sc: 500 });
await freezePlayer(player.publicKey);
const proposal = await db.getLatestFreezeProposal();
await simulateCommunityVote(proposal.id, 'CLEAR', 0.3); // 30% quorum, majority CLEAR
const balance = await db.getSCBalance(player.publicKey);
expect(balance).toBe(500); // Restored
});
});describe('Merkle Ledger Integrity', () => {
it('detects tampered transaction', async () => {
await populateTestLedger(100);
// Tamper with a transaction in the middle
await db.updateTransaction(50, { amount: 99999 });
const result = await verifyLedgerIntegrity();
expect(result.valid).toBe(false);
expect(result.failedAt).toBeDefined();
});
it('verifies clean ledger from genesis', async () => {
await populateTestLedger(1000);
const result = await verifyLedgerIntegrity();
expect(result.valid).toBe(true);
});
});describe('Governance Protocol', () => {
it('enforces quorum requirements', async () => {
const proposal = await createTestProposal({ type: 'STANDARD', quorum: 0.15 });
await createTestVoters(100); // 100 eligible voters
// Only 10 votes — below 15% quorum
await castTestVotes(proposal.id, 10, 'APPROVE');
const result = await tallyProposal(proposal.id);
expect(result.passed).toBe(false);
expect(result.quorumMet).toBe(false);
});
it('requires supermajority for constitutional proposals', async () => {
const proposal = await createTestProposal({ type: 'CONSTITUTIONAL' });
await createTestVoters(1000);
// 55% vote APPROVE — meets quorum but not supermajority (60% required)
await castTestVotes(proposal.id, 550, 'APPROVE');
await castTestVotes(proposal.id, 450, 'REJECT');
const result = await tallyProposal(proposal.id);
expect(result.passed).toBe(false);
expect(result.winnerFraction).toBeCloseTo(0.55);
});
});20 —
# docker-compose.yml for relay node
version: '3.8'
services:
relay:
image: ghcr.io/restore-protocol/relay:latest
ports:
- "8080:8080"
environment:
- PORT=8080
- STORAGE_PATH=/data/relay
- MAX_CONNECTIONS=10000
- ALLOWED_KINDS=1001,1002,1003,1004,1005,1006,1010,1011
volumes:
- relay-data:/data/relay
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
ipfs-pin:
image: ghcr.io/restore-protocol/ipfs-pin:latest
environment:
- PIN_STRATEGY=high-value # Pin verification photos + ledger snapshots
volumes:
- ipfs-data:/data/ipfs
restart: unless-stopped
volumes:
relay-data:
ipfs-data:# Minimum specs: 8GB VRAM GPU, 16GB RAM, 100Mbps connection python run_node.py \ --model meta-llama/Meta-Llama-3-8B-Instruct \ --port 8080 \ --max-batch-size 4 \ --sc-wallet /path/to/wallet.json # SC rewards sent here
// Nodes self-report health metrics to the P2P network
async function broadcastNodeHealth(): Promise<void> {
const metrics = {
uptime: process.uptime(),
storageAvailableGB: await getAvailableStorage(),
replicationStatus: await checkReplicationStatus(),
inferenceContributionTokens: await getInferenceContribution(),
peersConnected: p2pNode.getPeers().length,
ledgerHeight: await getLedgerHeight()
};
await broadcastNostrEvent({
kind: 1006,
content: JSON.stringify(metrics),
tags: [['type', 'node-health']]
});
}
setInterval(broadcastNodeHealth, 5 * 60 * 1000); // Every 5 minutes21 —
# Fork the relevant repository: # app: https://github.com/restore-protocol/app # relay: https://github.com/restore-protocol/relay # petals-node: https://github.com/restore-protocol/petals-node # ipfs-node: https://github.com/restore-protocol/ipfs-node # contracts: https://github.com/restore-protocol/governance-contracts git checkout -b feat/your-feature-name npm test # Must pass npm run lint # Must pass git push origin feat/your-feature-name # Open PR — all PRs reviewed by community, not a core team
| Contribution | SC Reward |
|---|---|
| Run a relay node (per day uptime) | 10–30 SC |
| Run an IPFS pinning node (per day) | 5–15 SC |
| Run a Petals compute node (residential GPU, per day) | 10–50 SC |
| Run a Petals compute node (data centre, per day) | 100–500 SC |
| Merged code contribution (judged by complexity) | 50–2000 SC |
| Documentation improvement | 10–100 SC |
| Security disclosure (responsible) | 500–5000 SC |
All protocol changes go through community vote. No pull request can alter the protocol without a corresponding governance proposal passing. Code is the implementation of the community's decision, not the decision itself.
Licence: Public Domain — copy, fork, build, share without restriction
Last Updated: March 2026 | Version 2.1
See also: WHITEPAPER-v2.md
The code is public domain. The protocol is the governance. Anyone who disagrees with the direction can fork it — that right is permanent and irrevocable.
Build it. Run it. Run your own version. That's the point.
community
Join the SudoSupport Discord
Connect with the sudosupport.consulting community — builders, local coordinators, and early network members.