guide to agentprivacy
Browse collections
โœจVisualise
Connect with Star
Your VTA, your chosen perspective

The planned connection uses your VTA and the Trust Spanning Protocol to carry a scoped exchange for you or your agent. You choose what is presented; the receiving service checks the request before a view is shared.

This guide has no VTA connection adapter yet. Opening Star does not connect an identity or send a key.

Open Star โ†— ยท Inspect your City Key โ†—
guide / Guide / Guide โ€” Build Guide

posture: 110010
dims: protection delegation computation
by: keeper

Build Guide

Step-by-step implementation from zero to production


Overview

This guide walks through building the complete Proverb Revelation Protocol in 4 phases:

  1. Foundation (Week 1): Zcash + Database
  2. Backend (Week 2): TEE + AI Integration
  3. Frontend (Week 3): User Interface
  4. Production (Week 4): Testing + Deployment

Phase 1: Foundation (Week 1)

Goals

  • Zcash light client syncing
  • PostgreSQL operational
  • Basic monitoring working
  • Memo parsing functional

Step 1.1: Initialize Oracle Swordsman Project

cd ~/proverb-protocol/oracle-swordsman

# For TypeScript
npm init -y
npm install --save \
    axios \
    pg \
    dotenv \
    winston \
    express

# Note: Nillion SDK is optional (use --omit=optional if needed)
# npm install --save @nillion/client-web

npm install --save-dev \
    @types/node \
    @types/pg \
    typescript \
    ts-node \
    nodemon

# Create tsconfig.json
cat > tsconfig.json << 'EOF'
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
EOF

# Create package.json scripts
cat > package.json << 'EOF'
{
  "name": "oracle-swordsman",
  "version": "1.0.0",
  "scripts": {
    "dev": "nodemon src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "test": "ts-node src/test.ts"
  }
}
EOF

Step 1.2: Create Configuration Module

// src/config.ts
import dotenv from 'dotenv';
dotenv.config();

export const config = {
  // Nillion
  nillion: {
    apiKey: process.env.NILLION_API_KEY!,
    network: process.env.NILLION_NETWORK || 'testnet',
  },
  
  // NEAR Cloud AI
  near: {
    apiKey: process.env.NEAR_API_KEY!,
    model: process.env.NEAR_MODEL || 'gpt oss 120b',
    endpoint: 'https://cloud.near.ai/v1/verify',
  },
  
  // IPFS
  ipfs: {
    jwt: process.env.PINATA_JWT!,
    gateway: process.env.PINATA_GATEWAY || 'https://gateway.pinata.cloud',
    spellbookCid: process.env.SPELLBOOK_CID!,
  },
  
  // Zcash
  zcash: {
    network: process.env.ZCASH_NETWORK || 'testnet',
    server: process.env.ZCASH_SERVER || 'https://zec.rocks:443',
    dataDir: process.env.ZCASH_DATA_DIR!,
  },
  
  // Database
  database: {
    url: process.env.DATABASE_URL!,
  },
  
  // Oracle settings
  oracle: {
    checkInterval: parseInt(process.env.ORACLE_CHECK_INTERVAL || '30') * 1000,
    retryAttempts: parseInt(process.env.ORACLE_RETRY_ATTEMPTS || '3'),
    retryDelay: parseInt(process.env.ORACLE_RETRY_DELAY || '60') * 1000,
  },
  
  // Economic model
  economics: {
    proverb_cost: 0.01,
    public_split: 0.618,
    private_split: 0.382,
    network_fee: 0.0001,
  },
};

// Validate required env vars
const required = [
  'NILLION_API_KEY',
  'NEAR_API_KEY',
  'PINATA_JWT',
  'SPELLBOOK_CID',
  'ZCASH_DATA_DIR',
  'DATABASE_URL',
];

for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
}

Step 1.3: Create Database Module

// src/database.ts
import { Pool, QueryResult } from 'pg';
import { config } from './config';

const pool = new Pool({
  connectionString: config.database.url,
});

export interface Submission {
  id: number;
  tracking_code: string;
  sender_address: string;
  proverb_text: string;
  amount_zec: number;
  txid: string;
  status: string;
  created_at: Date;
}

export interface Verification {
  id: number;
  submission_id: number;
  ai_provider: string;
  quality_score: number;
  matched_act: string;
  reasoning: string;
  verified_at: Date;
}

export interface Inscription {
  id: number;
  submission_id: number;
  public_txid: string;
  private_txid: string;
  public_amount: number;
  private_amount: number;
  inscribed_at: Date;
}

export const db = {
  // Test connection
  async testConnection(): Promise<boolean> {
    try {
      await pool.query('SELECT 1');
      return true;
    } catch (error) {
      console.error('Database connection failed:', error);
      return false;
    }
  },

  // Create submission
  async createSubmission(data: {
    tracking_code: string;
    sender_address: string;
    proverb_text: string;
    amount_zec: number;
    txid: string;
    memo_text?: string;
  }): Promise<Submission> {
    const result = await pool.query(
      `INSERT INTO submissions 
       (tracking_code, sender_address, proverb_text, amount_zec, txid, memo_text, status)
       VALUES ($1, $2, $3, $4, $5, $6, 'pending')
       RETURNING *`,
      [
        data.tracking_code,
        data.sender_address,
        data.proverb_text,
        data.amount_zec,
        data.txid,
        data.memo_text,
      ]
    );
    return result.rows[0];
  },

  // Get pending submissions
  async getPendingSubmissions(): Promise<Submission[]> {
    const result = await pool.query(
      `SELECT * FROM submissions WHERE status = 'pending' ORDER BY created_at ASC`
    );
    return result.rows;
  },

  // Update submission status
  async updateSubmissionStatus(id: number, status: string): Promise<void> {
    await pool.query(
      `UPDATE submissions SET status = $1 WHERE id = $2`,
      [status, id]
    );
  },

  // Create verification
  async createVerification(data: {
    submission_id: number;
    ai_provider: string;
    quality_score: number;
    matched_act: string;
    reasoning: string;
  }): Promise<Verification> {
    const result = await pool.query(
      `INSERT INTO verifications 
       (submission_id, ai_provider, quality_score, matched_act, reasoning)
       VALUES ($1, $2, $3, $4, $5)
       RETURNING *`,
      [
        data.submission_id,
        data.ai_provider,
        data.quality_score,
        data.matched_act,
        data.reasoning,
      ]
    );
    return result.rows[0];
  },

  // Create inscription
  async createInscription(data: {
    submission_id: number;
    public_txid: string;
    private_txid: string;
    public_amount: number;
    private_amount: number;
    network_fee: number;
  }): Promise<Inscription> {
    const result = await pool.query(
      `INSERT INTO inscriptions 
       (submission_id, public_txid, private_txid, public_amount, private_amount, network_fee)
       VALUES ($1, $2, $3, $4, $5, $6)
       RETURNING *`,
      [
        data.submission_id,
        data.public_txid,
        data.private_txid,
        data.public_amount,
        data.private_amount,
        data.network_fee,
      ]
    );
    return result.rows[0];
  },

  // Get submission by tracking code
  async getSubmissionByTrackingCode(code: string): Promise<Submission | null> {
    const result = await pool.query(
      `SELECT * FROM submissions WHERE tracking_code = $1`,
      [code]
    );
    return result.rows[0] || null;
  },

  // Get verification for submission
  async getVerification(submissionId: number): Promise<Verification | null> {
    const result = await pool.query(
      `SELECT * FROM verifications WHERE submission_id = $1`,
      [submissionId]
    );
    return result.rows[0] || null;
  },

  // Get stats
  async getStats(): Promise<any> {
    const result = await pool.query(`SELECT * FROM proverb_stats`);
    return result.rows[0];
  },

  // Close pool
  async close(): Promise<void> {
    await pool.end();
  },
};

Step 1.4: Create Zcash Client Module

// src/zcash-client.ts
import { exec } from 'child_process';
import { promisify } from 'util';
import { config } from './config';

const execAsync = promisify(exec);

export interface ZcashTransaction {
  txid: string;
  amount: number;
  address: string;
  memo?: string;
  confirmations: number;
  blockHeight?: number;
}

export class ZcashClient {
  private cliPath = 'zecwallet-cli';
  
  // Execute zecwallet-cli command
  private async execute(command: string): Promise<string> {
    const fullCommand = `${this.cliPath} --server ${config.zcash.server} --data-dir ${config.zcash.dataDir} --command "${command}"`;
    
    try {
      const { stdout, stderr } = await execAsync(fullCommand);
      if (stderr) {
        console.error('Zcash CLI error:', stderr);
      }
      return stdout;
    } catch (error: any) {
      throw new Error(`Zcash CLI failed: ${error.message}`);
    }
  }

  // Get list of transactions
  async listTransactions(): Promise<ZcashTransaction[]> {
    const output = await this.execute('list');
    // Parse output (format depends on zecwallet-cli version)
    return this.parseTransactionList(output);
  }

  // Get new transactions since last check
  async getNewTransactions(lastHeight: number): Promise<ZcashTransaction[]> {
    const allTxs = await this.listTransactions();
    return allTxs.filter(tx => 
      tx.blockHeight && tx.blockHeight > lastHeight && tx.confirmations > 0
    );
  }

  // Send transaction
  async sendTransaction(
    to: string,
    amount: number,
    memo?: string
  ): Promise<string> {
    const memoArg = memo ? `--memo "${memo}"` : '';
    const command = `send ${to} ${amount} ${memoArg}`;
    const output = await this.execute(command);
    return this.parseTxid(output);
  }

  // Get balance
  async getBalance(): Promise<{ transparent: number; shielded: number }> {
    const output = await this.execute('balance');
    return this.parseBalance(output);
  }

  // Sync blockchain
  async sync(): Promise<void> {
    await this.execute('sync');
  }

  // Get current block height
  async getBlockHeight(): Promise<number> {
    const output = await this.execute('height');
    return parseInt(output.trim());
  }

  // Parse transaction list (customize based on actual output)
  private parseTransactionList(output: string): ZcashTransaction[] {
    // Implementation depends on zecwallet-cli output format
    // This is a placeholder
    const transactions: ZcashTransaction[] = [];
    const lines = output.split('\n').filter(l => l.trim());
    
    for (const line of lines) {
      // Parse each line based on actual format
      // Example format: "txid | amount | address | memo | confirmations"
      transactions.push({
        txid: '', // parse from line
        amount: 0, // parse from line
        address: '', // parse from line
        memo: undefined, // parse from line
        confirmations: 0, // parse from line
      });
    }
    
    return transactions;
  }

  // Parse balance output
  private parseBalance(output: string): { transparent: number; shielded: number } {
    // Parse balance from output
    return {
      transparent: 0, // parse from output
      shielded: 0, // parse from output
    };
  }

  // Parse transaction ID from send output
  private parseTxid(output: string): string {
    // Extract txid from output
    const match = output.match(/[a-f0-9]{64}/);
    return match ? match[0] : '';
  }
}

export const zcashClient = new ZcashClient();

Step 1.5: Test Foundation

// src/test-foundation.ts
import { db } from './database';
import { zcashClient } from './zcash-client';
import { config } from './config';

async function testFoundation() {
  console.log('Testing Foundation...\n');

  // Test 1: Configuration
  console.log('1. Configuration:');
  console.log('   Nillion API Key:', config.nillion.apiKey.substring(0, 10) + '...');
  console.log('   NEAR Cloud AI API Key:', config.near.apiKey.substring(0, 10) + '...');
  console.log('   Spellbook CID:', config.ipfs.spellbookCid);
  console.log('   โœ“ Configuration loaded\n');

  // Test 2: Database
  console.log('2. Database:');
  const dbConnected = await db.testConnection();
  console.log('   Connection:', dbConnected ? 'โœ“ Connected' : 'โœ— Failed');
  
  const stats = await db.getStats();
  console.log('   Stats:', stats);
  console.log('   โœ“ Database operational\n');

  // Test 3: Zcash
  console.log('3. Zcash:');
  try {
    const balance = await zcashClient.getBalance();
    console.log('   Balance:', balance);
    
    const height = await zcashClient.getBlockHeight();
    console.log('   Height:', height);
    console.log('   โœ“ Zcash client working\n');
  } catch (error) {
    console.log('   โœ— Zcash client error:', error);
  }

  // Test 4: Create test submission
  console.log('4. Test Submission:');
  try {
    const submission = await db.createSubmission({
      tracking_code: 'TEST-' + Date.now(),
      sender_address: 't1test...',
      proverb_text: 'Privacy requires separation, not mere intention.',
      amount_zec: 0.01,
      txid: 'test_' + Date.now(),
    });
    console.log('   Created:', submission.id);
    console.log('   โœ“ Submission created\n');
  } catch (error) {
    console.log('   โœ— Submission error:', error);
  }

  console.log('Foundation tests complete!');
  await db.close();
}

testFoundation().catch(console.error);

Run test:

npm run test

Expected Output:

Testing Foundation...

1. Configuration:
   Nillion API Key: nillion_xxxxxxxxxx...
   NEAR Cloud AI API Key: xxxxxxxxxx...
   Spellbook CID: QmXxxx...
   โœ“ Configuration loaded

2. Database:
   Connection: โœ“ Connected
   Stats: { total_submissions: 0, completed: 0, pending: 0 }
   โœ“ Database operational

3. Zcash:
   Balance: { transparent: 0.5, shielded: 0 }
   Height: 2345678
   โœ“ Zcash client working

4. Test Submission:
   Created: 1
   โœ“ Submission created

Foundation tests complete!

Phase 2: Backend (Week 2)

Goals

  • IPFS integration working
  • AI verification functional
  • Nillion TEE deployed
  • Transaction signing operational

Step 2.1: Create IPFS Client

// src/ipfs-client.ts
import axios from 'axios';
import { config } from './config';

export interface SpellbookAct {
  id: string;
  title: string;
  description: string;
  category: string;
  keywords: string[];
}

export interface Spellbook {
  version: string;
  created_at: string;
  description: string;
  acts: SpellbookAct[];
}

export class IPFSClient {
  private gateway = config.ipfs.gateway;
  private spellbookCid = config.ipfs.spellbookCid;
  private cache: Spellbook | null = null;

  // Fetch spellbook from IPFS
  async fetchSpellbook(): Promise<Spellbook> {
    if (this.cache) {
      return this.cache;
    }

    const url = `${this.gateway}/ipfs/${this.spellbookCid}`;
    
    try {
      const response = await axios.get(url, {
        timeout: 10000,
        headers: {
          'Accept': 'application/json',
        },
      });
      
      this.cache = response.data;
      return this.cache;
    } catch (error: any) {
      throw new Error(`Failed to fetch spellbook: ${error.message}`);
    }
  }

  // Get specific act by ID
  async getAct(actId: string): Promise<SpellbookAct | null> {
    const spellbook = await this.fetchSpellbook();
    return spellbook.acts.find(act => act.id === actId) || null;
  }

  // Search acts by keyword
  async searchActs(keyword: string): Promise<SpellbookAct[]> {
    const spellbook = await this.fetchSpellbook();
    const lowerKeyword = keyword.toLowerCase();
    
    return spellbook.acts.filter(act =>
      act.keywords.some(k => k.toLowerCase().includes(lowerKeyword)) ||
      act.title.toLowerCase().includes(lowerKeyword) ||
      act.description.toLowerCase().includes(lowerKeyword)
    );
  }

  // Clear cache
  clearCache(): void {
    this.cache = null;
  }
}

export const ipfsClient = new IPFSClient();

Step 2.2: Create AI Verifier

// src/near-verifier.ts
import axios from 'axios';
import { config } from './config';
import { Spellbook } from './ipfs-client';

export interface VerificationResult {
  quality_score: number;
  matched_act: string;
  reasoning: string;
  approved: boolean;
}

export class NearVerifier {
  private endpoint = config.near.endpoint;
  private apiKey = config.near.apiKey;
  private model = config.near.model;

  // Verify proverb
  async verify(proverb: string, spellbook: Spellbook): Promise<VerificationResult> {
    try {
      const response = await axios.post(
        this.endpoint,
        {
          model: this.model,
          proverb: proverb,
          spellbook_acts: spellbook.acts,
          context: 'privacy_protocol',
        },
        {
          headers: {
            'Authorization': `Bearer ${this.apiKey}`,
            'Content-Type': 'application/json',
          },
          timeout: 30000,
        }
      );

      return response.data;
    } catch (error: any) {
      console.error('NEAR Cloud AI verification failed:', error.message);
      
      // Fallback to basic pattern matching
      return this.fallbackVerification(proverb, spellbook);
    }
  }

  // Fallback verification (basic pattern matching)
  private fallbackVerification(proverb: string, spellbook: Spellbook): VerificationResult {
    const lowerProverb = proverb.toLowerCase();
    
    // Check for key privacy concepts
    const concepts = [
      'privacy', 'separation', 'architecture', 'delegation',
      'boundary', 'sovereignty', 'protection', 'encryption',
    ];
    
    const matchedConcepts = concepts.filter(concept =>
      lowerProverb.includes(concept)
    );
    
    const quality_score = Math.min(matchedConcepts.length / concepts.length, 0.7);
    
    // Try to match an act
    let matched_act = 'act-01-shield'; // default
    for (const act of spellbook.acts) {
      for (const keyword of act.keywords) {
        if (lowerProverb.includes(keyword.toLowerCase())) {
          matched_act = act.id;
          break;
        }
      }
    }
    
    return {
      quality_score,
      matched_act,
      reasoning: 'Fallback verification (AI unavailable). Basic pattern matching used.',
      approved: quality_score >= 0.5,
    };
  }
}

export const nearVerifier = new NearVerifier();

Step 2.3: Create Nillion Signer

// src/nillion-signer.ts
import { NillionClient, SecretSigner } from '@nillion/client-web';
import { config } from './config';

export class NillionSigner {
  private client: NillionClient;
  private keyStoreId: string | null = null;

  constructor() {
    this.client = new NillionClient({
      network: config.nillion.network,
      apiKey: config.nillion.apiKey,
    });
  }

  // Initialize and store Zcash spending key
  async initializeKey(privateKey: Buffer): Promise<string> {
    try {
      this.keyStoreId = await SecretSigner.storeKey({
        client: this.client,
        privateKey: privateKey,
        algorithm: 'ECDSA',
      });
      
      console.log('Zcash key stored in SecretSigner:', this.keyStoreId);
      return this.keyStoreId;
    } catch (error: any) {
      throw new Error(`Failed to store key: ${error.message}`);
    }
  }

  // Sign transaction
  async signTransaction(txHash: Buffer): Promise<Buffer> {
    if (!this.keyStoreId) {
      throw new Error('Key not initialized. Call initializeKey() first.');
    }

    try {
      const signature = await SecretSigner.sign({
        client: this.client,
        storeId: this.keyStoreId,
        message: txHash,
        algorithm: 'ECDSA',
      });
      
      return signature;
    } catch (error: any) {
      throw new Error(`Failed to sign transaction: ${error.message}`);
    }
  }

  // Get attestation
  async getAttestation(): Promise<string> {
    try {
      const attestation = await this.client.getAttestation();
      return attestation;
    } catch (error: any) {
      throw new Error(`Failed to get attestation: ${error.message}`);
    }
  }

  // Verify attestation
  async verifyAttestation(attestation: string): Promise<boolean> {
    try {
      const valid = await this.client.verifyAttestation(attestation);
      return valid;
    } catch (error: any) {
      console.error('Attestation verification failed:', error.message);
      return false;
    }
  }
}

export const nillionSigner = new NillionSigner();

Step 2.4: Create Main Oracle Loop

// src/index.ts
import { config } from './config';
import { db } from './database';
import { zcashClient } from './zcash-client';
import { ipfsClient } from './ipfs-client';
import { nearVerifier } from './near-verifier';
import { nillionSigner } from './nillion-signer';

let lastProcessedHeight = 0;
let isProcessing = false;

async function processSubmissions() {
  if (isProcessing) {
    console.log('Already processing, skipping...');
    return;
  }

  isProcessing = true;

  try {
    // Get current height
    const currentHeight = await zcashClient.getBlockHeight();
    
    // Get new transactions
    const newTxs = await zcashClient.getNewTransactions(lastProcessedHeight);
    
    console.log(`Found ${newTxs.length} new transactions`);
    
    for (const tx of newTxs) {
      await processTransaction(tx);
    }
    
    lastProcessedHeight = currentHeight;
  } catch (error) {
    console.error('Error processing submissions:', error);
  } finally {
    isProcessing = false;
  }
}

async function processTransaction(tx: any) {
  try {
    console.log(`Processing transaction: ${tx.txid}`);
    
    // Extract tracking code and proverb from memo
    const { tracking_code, proverb } = parseMemo(tx.memo);
    
    // Check if already processed
    const existing = await db.getSubmissionByTrackingCode(tracking_code);
    if (existing) {
      console.log(`Already processed: ${tracking_code}`);
      return;
    }
    
    // Create submission
    const submission = await db.createSubmission({
      tracking_code,
      sender_address: tx.address,
      proverb_text: proverb,
      amount_zec: tx.amount,
      txid: tx.txid,
      memo_text: tx.memo,
    });
    
    console.log(`Created submission: ${submission.id}`);
    
    // Fetch spellbook
    const spellbook = await ipfsClient.fetchSpellbook();
    
    // Verify with AI
    const verification = await nearVerifier.verify(proverb, spellbook);
    
    // Save verification
    await db.createVerification({
      submission_id: submission.id,
      ai_provider: 'near',
      quality_score: verification.quality_score,
      matched_act: verification.matched_act,
      reasoning: verification.reasoning,
    });
    
    console.log(`Verification complete: ${verification.quality_score}`);
    
    if (verification.approved) {
      // Sign and inscribe
      await inscribeProverb(submission, proverb);
    } else {
      await db.updateSubmissionStatus(submission.id, 'rejected');
      console.log(`Submission rejected: quality too low`);
    }
    
  } catch (error) {
    console.error(`Error processing transaction:`, error);
  }
}

async function inscribeProverb(submission: any, proverb: string) {
  try {
    const publicAmount = config.economics.proverb_cost * config.economics.public_split;
    const privateAmount = config.economics.proverb_cost * config.economics.private_split;
    
    // Create inscription transaction (public - to spellbook transparent address)
    const publicTxid = await zcashClient.sendTransaction(
      config.zcash.publicInscriptionAddress, // Spellbook's transparent address
      publicAmount,
      `PROVERB:${submission.tracking_code}:${proverb}` // Proverb + proof of revelation
    );
    
    // Create shielded transaction (private)
    const privateTxid = await zcashClient.sendTransaction(
      'SHIELDED_ADDRESS', // Replace with actual shielded address
      privateAmount
    );
    
    // Save inscription
    await db.createInscription({
      submission_id: submission.id,
      public_txid: publicTxid,
      private_txid: privateTxid,
      public_amount: publicAmount,
      private_amount: privateAmount,
      network_fee: config.economics.network_fee,
    });
    
    await db.updateSubmissionStatus(submission.id, 'completed');
    
    console.log(`Inscription complete: ${publicTxid}`);
    
  } catch (error) {
    console.error(`Error inscribing:`, error);
    await db.updateSubmissionStatus(submission.id, 'failed');
  }
}

function parseMemo(memo: string): { tracking_code: string; proverb: string } {
  // Parse memo format: "TRACK:CODE|proverb text here"
  const parts = memo.split('|');
  const tracking_code = parts[0].replace('TRACK:', '');
  const proverb = parts[1] || '';
  return { tracking_code, proverb };
}

// Main loop
async function main() {
  console.log('Oracle Swordsman starting...');
  
  // Test connections
  const dbOk = await db.testConnection();
  if (!dbOk) {
    console.error('Database connection failed!');
    process.exit(1);
  }
  
  console.log('Database connected โœ“');
  
  // Get initial height
  lastProcessedHeight = await zcashClient.getBlockHeight();
  console.log(`Starting from height: ${lastProcessedHeight}`);
  
  // Start processing loop
  setInterval(processSubmissions, config.oracle.checkInterval);
  
  console.log(`Oracle running (check interval: ${config.oracle.checkInterval}ms)`);
}

main().catch(console.error);

Step 2.5: Test Backend

# Start oracle
npm run dev

# In another terminal, send test transaction
# (requires testnet ZEC and wallet setup)

Phase 3: Frontend (Week 3)

Goals

  • User submission interface
  • Status tracking
  • Quality score display
  • Mobile responsive

Step 3.1: Initialize Mage Agent

cd ~/proverb-protocol/mage-agent

# Create Next.js app
npx create-next-app@latest . --typescript --tailwind --app --no-src-dir

# Install dependencies
npm install axios qrcode.react

Step 3.2: Create API Route

// app/api/submit/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { proverb, tracking_code } = body;

    // Validate
    if (!proverb || !tracking_code) {
      return NextResponse.json(
        { error: 'Missing required fields' },
        { status: 400 }
      );
    }

    // Return payment info
    return NextResponse.json({
      success: true,
      payment_address: process.env.ZCASH_PAYMENT_ADDRESS,
      amount: 0.01,
      memo: `TRACK:${tracking_code}|${proverb}`,
      tracking_code,
    });
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

// app/api/status/[code]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/database'; // You'll need to set this up

export async function GET(
  request: NextRequest,
  { params }: { params: { code: string } }
) {
  try {
    const submission = await db.getSubmissionByTrackingCode(params.code);
    
    if (!submission) {
      return NextResponse.json(
        { error: 'Submission not found' },
        { status: 404 }
      );
    }

    const verification = await db.getVerification(submission.id);

    return NextResponse.json({
      status: submission.status,
      quality_score: verification?.quality_score,
      matched_act: verification?.matched_act,
      reasoning: verification?.reasoning,
      txid: submission.txid,
    });
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Step 3.3: Create Submission Page

// app/page.tsx
'use client';

import { useState } from 'react';
import axios from 'axios';
import QRCode from 'qrcode.react';

export default function Home() {
  const [proverb, setProverb] = useState('');
  const [trackingCode, setTrackingCode] = useState('');
  const [paymentInfo, setPaymentInfo] = useState<any>(null);
  const [status, setStatus] = useState<any>(null);

  const handleSubmit = async () => {
    const code = 'PROV-' + Date.now();
    setTrackingCode(code);

    const response = await axios.post('/api/submit', {
      proverb,
      tracking_code: code,
    });

    setPaymentInfo(response.data);
  };

  const checkStatus = async () => {
    const response = await axios.get(`/api/status/${trackingCode}`);
    setStatus(response.data);
  };

  return (
    <main className="min-h-screen p-8 max-w-2xl mx-auto">
      <h1 className="text-4xl font-bold mb-8">Proverb Revelation Protocol</h1>

      {!paymentInfo ? (
        <div>
          <textarea
            value={proverb}
            onChange={(e) => setProverb(e.target.value)}
            placeholder="Enter your proverb here..."
            className="w-full p-4 border rounded-lg mb-4"
            rows={6}
          />
          <button
            onClick={handleSubmit}
            className="bg-blue-600 text-white px-6 py-2 rounded-lg"
          >
            Submit Proverb
          </button>
        </div>
      ) : (
        <div>
          <h2 className="text-2xl mb-4">Payment Required</h2>
          <p>Send exactly 0.01 ZEC to:</p>
          <code className="block bg-gray-100 p-2 my-4">
            {paymentInfo.payment_address}
          </code>
          
          <p className="mb-2">Include this memo:</p>
          <code className="block bg-gray-100 p-2 mb-4 text-xs">
            {paymentInfo.memo}
          </code>

          <QRCode value={`zcash:${paymentInfo.payment_address}?amount=0.01&memo=${paymentInfo.memo}`} />

          <div className="mt-8">
            <p>Tracking Code: <code>{trackingCode}</code></p>
            <button
              onClick={checkStatus}
              className="mt-4 bg-green-600 text-white px-6 py-2 rounded-lg"
            >
              Check Status
            </button>
          </div>

          {status && (
            <div className="mt-4 p-4 border rounded-lg">
              <p>Status: <strong>{status.status}</strong></p>
              {status.quality_score && (
                <>
                  <p>Quality Score: <strong>{status.quality_score}</strong></p>
                  <p>Matched Act: <strong>{status.matched_act}</strong></p>
                  <p className="mt-2 text-sm">{status.reasoning}</p>
                </>
              )}
            </div>
          )}
        </div>
      )}
    </main>
  );
}

Phase 4: Production (Week 4)

Step 4.1: Testing Checklist

# Test Oracle
- [ ] Can detect Zcash transactions
- [ ] Parses memo correctly
- [ ] Fetches spellbook from IPFS
- [ ] AI verification works
- [ ] Signs with Nillion
- [ ] Broadcasts transactions
- [ ] Updates database correctly

# Test Frontend
- [ ] Can submit proverb
- [ ] Displays payment info
- [ ] Shows QR code
- [ ] Tracks status
- [ ] Shows quality score

# Integration Test
- [ ] End-to-end: submission โ†’ inscription
- [ ] 61.8/38.2 golden ratio split correct
- [ ] Tracking codes match
- [ ] All data persisted

Step 4.2: Production Deployment

# Build oracle
cd oracle-swordsman
npm run build

# Create systemd service
sudo cat > /etc/systemd/system/oracle-swordsman.service << 'EOF'
[Unit]
Description=Oracle Swordsman
After=network.target

[Service]
Type=simple
User=your_user
WorkingDirectory=/home/your_user/proverb-protocol/oracle-swordsman
ExecStart=/usr/bin/node dist/index.js
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

# Start service
sudo systemctl enable oracle-swordsman
sudo systemctl start oracle-swordsman

# Build frontend
cd ../mage-agent
npm run build

# Deploy with Nginx
# Configure reverse proxy for Next.js

SpellWeb Feature

The SpellWeb is an interactive graph visualization showing your spell constellation. It includes several advanced features:

Features

  1. Constellation Mode - View all spells connected by grimoire, sequence, and custom links
  2. Draw Mode - Click two nodes to create custom connections between them
  3. Randomise Layout - Scatter nodes and let the physics simulation reorganize them
  4. Expand/Fullscreen - View the full graph in fullscreen mode (press Escape to exit)
  5. Grimoire Filter Panel - Toggle individual grimoires on/off to focus on specific spell groups

Key Files

src/
โ”œโ”€โ”€ components/spellweb/
โ”‚   โ”œโ”€โ”€ SpellwebViewer.tsx         # Basic viewer (used on /spells)
โ”‚   โ””โ”€โ”€ SpellwebViewerAgentic.tsx  # Enhanced viewer with draw/constellation modes
โ”œโ”€โ”€ lib/spellweb/
โ”‚   โ”œโ”€โ”€ types.ts                   # Node, Link, DrawnLink type definitions
โ”‚   โ”œโ”€โ”€ builder.ts                 # Builds graph from selected spells
โ”‚   โ””โ”€โ”€ labels.ts                  # Grimoire colors, emojis, short labels
โ””โ”€โ”€ app/web/page.tsx               # Web page with grimoire filter panel

SpellwebViewerAgentic Props

interface SpellwebViewerAgenticProps {
  data: SpellwebData;           // Nodes and links from builder
  grimoireFilter?: string[];    // Active grimoire IDs to show
  onNodeSelect?: (node) => void;// Callback when node is clicked
  fullHeight?: boolean;         // Fill container height
}

Link Types

Type Description Visual
grimoire Spell โ†’ grimoire hub Gray solid
sequence Spell โ†’ next spell Gray dashed
cluster Skill โ†’ skills hub Gray solid
constellation Cross-grimoire Orange solid
drawn User-drawn (temporary) Green dashed
saved User-saved (persistent) Green dashed

localStorage Persistence

User-drawn maps are saved to agentprivacy-web-saved-map in localStorage. When you click "Save map" in Draw mode, your custom links persist and appear in Constellation mode.


Next Steps

โœ… Build Complete!

You now have:

  • Working Oracle Swordsman
  • Functional Mage Agent
  • Complete integration
  • Production deployment
  • SpellWeb visualization with draw/constellation modes

Next: Reference docs/04-API_REFERENCE.md for detailed code patterns

Then: Use docs/05-ROADMAP.md to track your integration checklist