Skip to main content

Documentation Index

Fetch the complete documentation index at: https://prophet.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Prophet has two autonomous agents running off-chain:
  • Oracle Agent — resolves markets by calling 0G Compute and posting verdicts on-chain
  • Market Maker Agent — seeds new markets with liquidity and returns settled funds to the pool
Both are TypeScript services built with ethers v6, @0glabs/0g-serving-broker, and @0gfoundation/0g-storage-ts-sdk. They run as a single process (npm run start) or independently. Repository path: agent/src/

Oracle Agent

File: agent/src/oracle/index.ts The oracle is Prophet’s core differentiator. It is the only component that can call postResolution() and revealPositions(). Without it, markets cannot resolve. With it, markets resolve deterministically, in under 2 minutes, with a permanent evidence trail on 0G Storage.

Startup: Catch-Up Scan

On startup, before listening for new events, the oracle scans all existing markets to handle any that were missed while the agent was offline:
async function catchUpOnPendingMarkets() {
  const markets = await factory.getMarkets();
  
  for (const marketAddress of markets) {
    const market = getMarketContract(marketAddress);
    const status = await market.status();
    const deadline = await market.deadline();
    
    switch (status) {
      case MarketStatus.Open:
        // Check if deadline has passed but triggerResolution not called
        if (Date.now() / 1000 > deadline) {
          await market.triggerResolution();
        }
        break;

      case MarketStatus.PendingResolution:
        // Oracle hasn't run yet — run now
        await handleResolutionTriggered(marketAddress);
        break;

      case MarketStatus.Challenged:
        const challenger = await market.challenger();
        const challengeDeadline = await market.challengeDeadline();
        
        if (challenger !== ethers.ZeroAddress) {
          // Real challenge filed — run second inference
          await handleChallenge(marketAddress);
        } else if (Date.now() / 1000 > challengeDeadline) {
          // No challenge, window expired — finalize
          await market.finalizeResolution();
        }
        break;

      case MarketStatus.Resolved:
        const revealed = await vault.hasRevealed(marketAddress, ethers.ZeroAddress);
        if (!revealed) {
          // Positions not yet revealed — reveal now
          await revealPositions(marketAddress);
        }
        break;
    }
  }
}
This means the oracle is stateless — it can be restarted at any time and will pick up exactly where it left off.

Event Listeners

After the catch-up scan, the oracle attaches listeners to three events:
// Event 1: Market deadline passed, oracle should resolve
factory.on('ResolutionTriggered', async (marketAddress, timestamp) => {
  await handleResolutionTriggered(marketAddress);
});

// Event 2: Challenge filed against oracle's verdict
factory.on('ResolutionChallenged', async (marketAddress, challenger, bond) => {
  await handleChallenge(marketAddress);
});

// Event 3: Resolution finalized — time to reveal positions
factory.on('ResolutionFinalized', async (marketAddress, verdict) => {
  await revealPositions(marketAddress);
});
Listener errors never crash the agent — they are caught, logged, and the listener continues.

Periodic Scan

In addition to event listeners, the oracle runs a full catch-up scan every 5 minutes. This handles:
  • Events missed due to RPC instability
  • Markets where triggerResolution was called by a third party while the agent was processing another market
  • Any edge case where the event was not received
setInterval(catchUpOnPendingMarkets, 5 * 60 * 1000);

Resolution Flow (handleResolutionTriggered)

async function handleResolutionTriggered(marketAddress: string) {
  // 1. Read metadata from 0G Storage
  const metadataHash = await marketContract.metadataHash();
  const metadata = await downloadFromStorage(metadataHash);
  
  // 2. Call 0G Compute
  const result = await callOracleInference(
    metadata.question,
    metadata.sources,
    metadata.deadline,
    oracleWallet
  );
  
  // 3. Check confidence
  if (result.confidence < 70) {
    logger.warn({ marketAddress, confidence: result.confidence }, 'Inconclusive — skipping');
    return;
  }
  
  // 4. Upload reasoning to 0G Storage
  const reasoningHash = await uploadToStorage({
    ...result,
    marketAddress,
    timestamp: new Date().toISOString()
  }, oracleWallet);
  
  // 5. Post verdict on-chain
  await marketContract.postResolution(result.verdict, reasoningHash);
}

Position Reveal (NaCl Decryption)

After ResolutionFinalized, the oracle fetches all encrypted positions from PositionVault, decrypts them using its private key, and calls revealPositions:
async function revealPositions(marketAddress: string) {
  const count = await vault.positionCount(marketAddress);
  
  const traders = [], directions = [], amounts = [], sigs = [];
  
  for (let i = 0; i < count; i++) {
    const { trader, encryptedPosition } = await vault.getPosition(marketAddress, i);
    
    // NaCl box decryption using oracle's private key
    const decrypted = decryptPosition(encryptedPosition, oraclePrivateKey);
    
    // Sign the revealed data (for PositionVault verification)
    const sig = await signReveal(marketAddress, trader, decrypted.direction, decrypted.amount);
    
    traders.push(trader);
    directions.push(decrypted.direction === 'YES');
    amounts.push(decrypted.amount);
    sigs.push(sig);
  }
  
  await vault.revealPositions(traders, directions, amounts, sigs);
}

Market Maker Agent

File: agent/src/market-maker/index.ts The market-maker ensures every market has liquidity from minute one. Without it, new markets have zero YES/NO reserves and the AMM cannot process trades.

Startup: Scan Existing Markets

async function startup() {
  const markets = await factory.getMarkets();
  
  for (const marketAddress of markets) {
    const market = getMarketContract(marketAddress);
    const status = await market.status();
    
    if (status === MarketStatus.Open) {
      const { yesReserve } = await market.getAmmState(ethers.ZeroAddress);
      
      if (yesReserve === 0n) {
        // Market not yet seeded — allocate and seed
        await seedMarket(marketAddress);
      }
    }
  }
}

Seeding a New Market

async function seedMarket(marketAddress: string) {
  const seedAmount = calculateSeedAmount(); // based on pool balance
  
  // 1. Get initial price estimate from 0G Compute
  const pricing = await callPricingInference(marketQuestion, mmWallet);
  
  // 2. Allocate from LiquidityPool
  await liquidityPool.allocateToMarket(marketAddress, seedAmount);
  
  // 3. Approve MarketContract to spend the allocated USDT
  await usdt.approve(marketAddress, seedAmount);
  
  // 4. Seed the AMM
  await marketContract.seedLiquidity(seedAmount);
  
  // 5. Push initial prices to frontend cache
  await pushPricesCache(marketAddress, pricing.yesPrice, pricing.noPrice);
  
  logger.info({ marketAddress, seedAmount }, 'Market seeded');
}

Queued Transaction System

The market-maker can receive many MarketCreated events in rapid succession (during demos or high activity periods). Submitting multiple transactions simultaneously causes nonce collisions on 0G Chain. The agent uses a simple FIFO queue:
const txQueue: Array<() => Promise<void>> = [];
let processing = false;

async function enqueue(fn: () => Promise<void>) {
  txQueue.push(fn);
  if (!processing) processQueue();
}

async function processQueue() {
  processing = true;
  while (txQueue.length > 0) {
    const task = txQueue.shift()!;
    try {
      await task();
    } catch (err) {
      logger.error(err, 'Transaction queue error');
    }
  }
  processing = false;
}
All seeding and repricing operations go through enqueue(). This serializes transactions and eliminates nonce issues.

Event Listener: New Markets

factory.on('MarketCreated', async (marketAddress, creator, question) => {
  await enqueue(() => seedMarket(marketAddress));
});

Recovery Loop: Settled Markets

After a market resolves, the collateral should return to the LiquidityPool. The market-maker runs a recovery loop that checks for resolved markets and returns their allocated liquidity:
setInterval(async () => {
  const markets = await factory.getMarkets();
  
  for (const marketAddress of markets) {
    const status = await marketContract.status();
    const allocation = await liquidityPool.marketAllocation(marketAddress);
    
    if (status === MarketStatus.Resolved && allocation > 0n) {
      await enqueue(() => liquidityPool.returnFromMarket(marketAddress, allocation));
    }
  }
}, REPRICE_INTERVAL_MS);

Shared Utilities

agent/src/shared/compute.ts

Functions:
  • initializeBroker(signer) — creates @0glabs/0g-serving-broker instance
  • topUpLedger(broker, provider, amount) — funds the billing ledger before inference
  • verifyProvider(broker, provider) — TEE attestation check
  • callOracleInference(question, sources, deadline, signer) — full resolution inference
  • callValidationInference(question, signer) — question validation (lighter prompt)
  • callPricingInference(question, signer) — initial price estimate
  • extractJSON(text) — strips markdown fences, parses JSON with error handling

agent/src/shared/storage.ts

Functions:
  • uploadToStorage(data, signer, maxRetries) — upload with retry + read-after-write verification
  • downloadFromStorage(rootHash, maxRetries) — download with retry
  • getIndexer() — returns Indexer pointed at turbo indexer URL

agent/src/shared/chain.ts

Functions:
  • getFactoryContract(signer) — ProphetFactory with signer
  • getMarketContract(address, signer) — MarketContract at address
  • getVaultContract(signer) — PositionVault with signer
  • getLiquidityPoolContract(signer) — LiquidityPool with signer
  • listenForEvents(contract, eventName, handler) — safe event listener wrapper

agent/src/shared/config.ts

// cfg() helper — typed env var access with defaults and validation
export function cfg(key: string, required = true): string {
  const value = process.env[key];
  if (!value && required) throw new Error(`Missing required env var: ${key}`);
  return value ?? '';
}

// Usage:
const rpc = cfg('OG_CHAIN_RPC');
const interval = parseInt(cfg('REPRICE_INTERVAL_MS', false) || '60000');

agent/src/shared/logger.ts

Structured JSON logging using pino (or equivalent). Every log entry includes:
  • timestamp
  • level (debug / info / warn / error)
  • component (oracle / mm / storage / compute)
  • Contextual fields (marketAddress, txHash, etc.)
logger.info({ marketAddress, verdict: true, confidence: 88 }, 'Resolution posted');
logger.error({ err, marketAddress }, 'Failed to call 0G Compute');
Log level is controlled by LOG_LEVEL env var. Default: info.

agent/src/shared/types.ts

Shared TypeScript interfaces:
interface OracleResponse {
  verdict: boolean | null;
  confidence: number;
  reasoning: string;
  evidenceSummary: string;
  sourcesChecked: string[];
  inconclusiveReason?: string;
}

interface MarketMetadata {
  question: string;
  deadline: string;
  category: string;
  sources: string[];
  createdAt: string;
  creatorAddress: string;
}

interface PositionData {
  direction: 'YES' | 'NO';
  amount: string; // bigint as string (avoid JSON precision loss)
}