Underlay

Underlay: onchain risk vault where traders combine Polymarket legs, priced by 0G AI, settled by CRE.

Underlay

Created At

ETHGlobal Cannes 2026

Project Description

Underlay is an onchain risk underwriting protocol built on Base Sepolia. Traders combine multiple Polymarket prediction market outcomes into a single multi-leg position — odds multiply across legs, and the combined risk is priced by a live LLM running on the 0G Compute network. The AI's reasoning and audit receipt are pinned permanently to 0G Storage as a Merkle root hash, which is written onchain with every position.

The protocol is underwritten by a community ERC-4626 vault. LPs deposit USDC, idle capital routes to Aave V3 for yield, and a reserve split underwrites winning payouts. When traders lose, stakes sweep into the vault and LP share price appreciates. When traders win, the reserve pays out — up to a configurable max payout ceiling.

Settlement runs through Chainlink CRE. A keeper workflow polls open positions, checks each leg against the Polymarket CLOB API for resolution, and calls resolveLeg onchain when a market closes. Once all legs resolve and a position is marked WON, a risk-tier delay window starts — 30 seconds for low risk, 60 for medium, 120 for high — enforced by SettlementManager before payout executes. The CRE workflow also cross-references crypto market outcomes against Chainlink ETH/USD and BTC/USD price feeds.

Sybil resistance is enforced via World ID — stakes above a configured threshold require a valid zero-knowledge proof verified onchain through Worldcoin's IWorldID contract, with nullifier tracking to prevent replay.

Underlay is fully onchain with no opaque backend logic. Every position has an audit trail.

How it's Made

Underlay is built as a full-stack DeFi protocol with five distinct layers — smart contracts, risk engine, settlement orchestration, frontend, and keeper infrastructure — all wired together across Base Sepolia, 0G Galileo testnet, Chainlink CRE staging, and Worldcoin.

Smart Contracts (Solidity + Foundry)

Two core contracts handle all onchain logic. RiskEngine.sol is the entry point — it validates positions, enforces leg count rules (2–10 legs), checks stake limits from both vault config and the AI-returned aiStakeLimit, verifies World ID proofs for high-stakes positions, and transfers USDC from the trader into the PositionBook. It uses OpenZeppelin's Ownable, ReentrancyGuard, and SafeERC20. World ID integration uses IWorldID.verifyProof() with a nullifier hash mapping to prevent proof replay — the same zk-proof can never be used twice.

SettlementManager.sol orchestrates the post-resolution lifecycle. Settlement delays are configurable storage variables (not constants), so we can set LOW=30s, MEDIUM=60s, HIGH=120s for demo purposes without redeploying. The onlyCRE modifier allows both the CRE forwarder address and address(this) — the latter enables resolveLegs() to call this.resolveLeg() internally without auth failures. resolveLegs() wraps each leg in a try/catch so a single reverting leg doesn't block the whole batch. executeSettlement() enforces the delay window, handles an optional challenge extension, and calls PositionBook.executePayout() to release the reserve to the trader.

0G Compute + Storage (AI Risk Scoring)

The risk engine (app/src/lib/server/risk-engine.ts) runs in a Next.js API route. It uses @0glabs/0g-serving-broker to discover live LLM services on the 0G Galileo network, picks the first non-image model (filtering out "image", "vision", "clip", "diffusion"), fetches request headers and endpoint metadata from the broker, then POSTs to /chat/completions with a structured risk assessment prompt. The current live model is qwen/qwen-2.5-7b-instruct.

Two notable hacks here. First: the @0glabs/0g-ts-sdk storage SDK uses XMLHttpRequest internally — a browser API that doesn't exist in Node.js server environments. We dynamically polyfill it at runtime: (globalThis as any).XMLHttpRequest = XHR before the SDK loads. Second: the SDK's indexer.upload() returns a tuple [result, error] where success is indicated by error = {} (empty object) — not null. Since if ({}) is truthy in JavaScript, our initial check always threw a false error. Fixed with Object.keys(uploadError).length > 0 to distinguish a real error from an empty success response. After upload, the Merkle root hash from file.merkleTree() is written onchain as riskAuditHash in submitPosition(). The positions page reads provider from localStorage (written at submission time) and renders it as a clickable storagescan-galileo.0g.ai link when stored on 0G.

Chainlink CRE Workflow

The settlement workflow (underlay-workflow/main.ts) runs on the Chainlink CRE DON using @chainlink/cre-sdk. It uses ConfidentialHTTPClient (not plain HTTPClient) for all Polymarket CLOB calls — requests are routed through the DON's TEE enclave, keeping responses private until consensus. The workflow runs on a cron trigger, reads open positions from PositionBook via EVMClient.callContract() at LAST_FINALIZED_BLOCK_NUMBER, checks each leg against clob.polymarket.com/markets/{conditionId}, and uses tokens[0] (always the YES-equivalent regardless of label) to determine outcome via winner === true || price > 0.99. Resolved legs are batched into resolveLegs() and submitted via runtime.report() with consensusIdenticalAggregation — multiple DON nodes must agree on the same batch before it lands onchain.

For the Connect the World prize, after resolving a crypto market, the workflow calls the Chainlink ETH/USD or BTC/USD price feed via latestRoundData(), extracts a price threshold from the market question text using a regex, and cross-references whether the onchain price agrees with Polymarket's outcome — logging AGREE ✓ or DISAGREE ⚠.

Standalone Keeper (GitHub Actions)

Because CRE simulation doesn't land onchain during development, we built a parallel keeper (cre-workflow/scripts/keeper.ts) using viem and viem/accounts. It runs two steps every tick: RESOLVE reads open positions and calls resolveLeg() directly from a funded wallet, and EXECUTE scans the last 9,000 blocks (public RPC's eth_getLogs limit) for SettlementInitiated events, deduplicates by position ID, checks isReadyToSettle(), and calls executeSettlement(). This runs on GitHub Actions on a */5 * * * * cron using npm install (not npm ci since the lockfile is gitignored) with the keeper private key stored as a repository secret.

World ID

WorldIdVerifyButton.tsx uses @worldcoin/idkit v4. When stake exceeds the configured gate threshold, the component fetches a signed RP context from /api/world-id/context, opens the IDKit widget, and on completion submits the proof to /api/world-id/verify which calls Worldcoin's Developer API at developer.world.org/api/v4/verify/{rpId}. The verified proof — root, nullifierHash, and 8-element proof array decoded via decodeAbiParameters — is then passed directly into submitPosition() as calldata for onchain ZK verification.

Polymarket Integration

Market browsing uses the Gamma API (gamma-api.polymarket.com) for listing and metadata, enriched with event data fetched in chunks of 20 to avoid query limits. Resolution checking uses the CLOB API (clob.polymarket.com/markets/{conditionId}) for exact single-market lookups — the Gamma API's ?conditionId= filter is broken and ignores the parameter entirely, returning 20 random markets. The titles API route proxies CLOB lookups server-side to avoid CORS. "Closing Soon" markets use order=endDate&ascending=true&end_date_min=now&end_date_max=now+24h with no cache (revalidate: 0) since they're time-sensitive.

Frontend

Built with Next.js 14 App Router, Tailwind, wagmi v2, and viem. The cart uses useWaitForTransactionReceipt to detect approval confirmation, then auto-submits the position. Payout display re-derives from stake × combinedOdds / 1_000_000n for settled positions since executePayout() zeroes the stored value after transfer. The animated node background is a canvas element using ResizeObserver with 48 drifting dots connected by proximity lines, all pointer-events-none so it doesn't interfere with interactions.

background image mobile

Join the mailing list

Get the latest news and updates

Underlay | ETHGlobal