RESTORE — Technical Blueprint · v2.1 · March 2026

How It
Actually Works.

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.

TypeScript libp2p · Nostr · IPFS BLE · ed25519 Public Domain

table of contents

01 —

Architecture Overview

┌──────────────────────────────────────────────────────────┐
│                   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    │
└──────────────────────────────────────────────────────────┘

Technology Stack

LayerTechnology
FrontendProgressive Web App (PWA), TypeScript
Local storageSQLite (via sql.js-httpvfs), IndexedDB
Networkinglibp2p, WebRTC data channels
Event protocolNostr (NIP-01 base + custom kinds 1001–1011)
Content storageIPFS / Helia (browser-compatible)
ProximityWeb Bluetooth API (BLE)
Cryptography@noble/ed25519, @noble/ciphers/chacha, @noble/hashes
Local inferencellama.cpp WASM / Ollama sidecar
P2P inferencePetals-compatible node network
BundlerVite
TestsVitest, Playwright


02 —

Quick Start

Run the App Locally

git clone https://github.com/restore-protocol/app
cd app
npm install
npm run dev
# → http://localhost:5173

Run a Relay Node

git clone https://github.com/restore-protocol/relay
cd relay
npm install
cp .env.example .env  # edit port and storage path
npm start

Run a Petals Compute Node

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

Run a Full IPFS Pinning Node

git clone https://github.com/restore-protocol/ipfs-node
cd ipfs-node
docker compose up -d

Environment Variables

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 —

Cryptographic Identity and Key Management

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.

Key Generation

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 };
}

Signing and Verification

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));
}

Wallet Backup and Recovery

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 —

BLE Handshake Protocol
Proof of Personhood

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.

UUIDs

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';

Advertising Data (Rotates Every 15 Minutes)

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);
}

Full Handshake Protocol

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;
}

Offline Queue (Encounters Without Internet)

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 —

Web of Trust and Sybil Resistance

PageRank-Style Trust Propagation

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;
  }
}

Velocity Limits

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 —

P2P Network Layer

libp2p Node Configuration

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'
};

IPFS Content Storage

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 —

The Merkle SC Ledger

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.

Merkle Chain Construction

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 —

Mutual Credit Engine

Credit Limit Calculations

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));
}

Cross-Community Trust Certificate

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);
}

Demurrage (Anti-Hoarding)

// 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 —

Circuit Breakers and Anomaly Detection

Tripwire Configuration

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
};

Anomaly Detection Engine

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 —

The Agent Architecture

Deterministic State Machine (90% of Interactions)

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
    });
  }
}

Agent Memory (Encrypted Local Storage)

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;
  }
}

Mission Generation

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 —

Distributed LLM Inference

Inference Router

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 };
  }
}

Token Starvation (Latency Reduction)

// 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 —

Governance Protocol

Proposal Lifecycle

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 —

World Agent System

World Agent Data Structure

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)
  };
}

World Agent Nomination and Seeding

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 —

Data Structures

Core Types

// 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 —

Nostr Event Protocol

RESTORE uses Nostr's event architecture for propagation: simple, cryptographically sound, censorship-resistant, requiring no central authority.

Event Kinds

KindDescriptionTags
`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]`

Canonical Event Structure

{
  "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"
}

Relay Selection Strategy

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 —

Latency Optimisation

Physical Buffer Pattern

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 };
  }
}

Token Starvation Pipeline

// 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 —

PWA and Offline-First Architecture

Service Worker (Offline Capability)

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());
});

Offline-First Data Model

// 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 —

Security Model and Attack Surface

Attack Vectors and Mitigations

Attack VectorMechanismMitigationStatus
GPS spoofingFake location dataBLE physical proximity replaces GPS as primary verification✅ Solved
AI-generated verification photosFake photo evidenceBLE challenge-response cannot be replicated without physical hardware✅ Solved
Sybil attack (fake accounts)Many fake accounts voting or earning SCWeb of Trust + velocity limits + physical handshake requirement✅ Mitigated
Exploit farm / SC farmingRapid SC generation from exploited missionCircuit breakers detect and freeze within 1 hour✅ Solved
51% governance attackConcentrated voting powerQuadratic voting (sqrt) + Proof of Personhood multiplier prevents whale dominance✅ Mitigated
Supply chain attackCompromised npm packageDependency pinning, SLSA provenance, reproducible builds🔄 In Progress
Private key theftStolen deviceKeys encrypted at rest; passphrase required to decrypt✅ Solved
Relay censorshipHostile relay dropping eventsMulti-relay broadcast + direct peer fallback✅ Solved
P2P inference poisoningMalicious inference nodeOutput validation, structured outputs only, results cross-checked🔄 In Progress

Cryptographic Primitives

// 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 —

Testing

BLE Handshake Tests

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);
  });
});

Circuit Breaker Tests

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
  });
});

Merkle Ledger Tests

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);
  });
});

Governance Tests

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 —

Deployment and Node Operations

Relay Node

# 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:

Petals Compute Node

# 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

Monitoring

// 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 minutes


21 —

Contributing

Code Contributions

# 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

Earn SC for Infrastructure Contributions

ContributionSC 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 improvement10–100 SC
Security disclosure (responsible)500–5000 SC

Governance Participation

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.

Resources

  • Whitepaper: WHITEPAPER-v2.md — philosophy and architecture
  • GitHub: github.com/restore-protocol (placeholder pending launch)
  • Docs: docs.restore.xyz (placeholder)
  • Discord: https://discord.gg/sjD4HRyTKP
  • Issues: github.com/restore-protocol/app/issues

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.

Version 2.1 · RESTORE / SOVEREIGN Platform · Public Domain
See also: WHITEPAPER-v2.md · github.com/restore-protocol (placeholder)
Join the Discord →

community

Join the SudoSupport Discord

Connect with the sudosupport.consulting community — builders, local coordinators, and early network members.

Join the Discord →