A Solana-native optimistic oracle for natural-language statements.
Opal treats assertions as true by default. If nobody disputes within a liveness window, the assertion resolves True. If disputed, it escalates through an LLM resolution round and (if challenged) a staked private vote.
Opal resolves rubric-relative truth, not absolute truth: every assertion ships its own Resolution Spec — the asserter's rubric for how the statement should be judged — and Opal applies that spec rather than adjudicating any universal reality. The same statement text can resolve differently across assertions. See ADR-0001.
Target use case: prediction-market resolution. Statements like "Kanye West's Delhi concert got postponed" can't be answered by price feeds or APIs — they need a purpose-built oracle, and they need someone to declare by what standard "postponed" is judged.
Status: this README describes Opal's full target design. Feature status is tracked with badges —
[Built]is implemented in the program today,[MVP-target]is committed but not yet built,[Vision]is post-MVP. Sections below are the design; see Current State for exactly what's live.
- Assert — someone posts a statement, a USDC bond, and a Resolution Spec (the rubric; stored off-chain on Arweave, its hash on-chain)
- Wait — liveness window where anyone can dispute
- Undisputed — if no dispute, resolves
True - Disputed — the first dispute triggers on-chain LLM resolution: a single trusted off-chain resolver posts the verdict via the resolver-gated
submit_llm_resolutioninstruction[Built]; the off-chain service that makes the LLM call is[MVP-target](the former 3-feed Switchboard council was removed per ADR-0002) - Challenged — if the LLM verdict is challenged, escalate to a per-dispute staked vote, kept private during the voting window via a MagicBlock ephemeral rollup (ADR-0003)
- Resolved — the final outcome is posted on-chain
The LLM layer is deliberately trusted, not trustless: a wrong verdict is challengeable into the staked vote, which is the real trust backstop.
True— verified under the specFalse— contradicted under the specUnresolvable— cannot be decided under the spec (ambiguous, conflicting, premature, or no outcome reached the vote's supermajority).[MVP-target]Settles no-fault: both bonds are returned, no one is slashed, and the assertion is voided (today any non-Trueoutcome is settled likeFalse) (ADR-0005).
The final escalation is a private, per-dispute, USDC-staked vote (today open_vote sets up the round, but real staking, tallying, and MagicBlock privacy are not yet built):
- Linear weight — 1 staked USDC = 1 vote. Sybil-neutral; whale dominance is deterred by slashing, not by a weight curve.
- Schelling-point slashing — losing-side voters are slashed and winning-side voters are paid from the losing side, so the honest answer under the spec is the focal point.
- Private — votes are sealed during the window via a MagicBlock ephemeral rollup; only the aggregate outcome is committed on-chain, which prevents a public tally from collapsing into a bandwagon.
- Supermajority — a single outcome must reach the configured
supermajority_bpsthreshold, otherwise the vote resolvesUnresolvable.
USDC is the single protocol asset for all bonds, voting stake, rewards, slashing, and treasury fees. The mint is a config field (currently pusd_mint on-chain; renaming to usdc_mint) so localnet/devnet can use a test mint. See ADR-0004.
# Install deps
bun install
# Build the program
anchor build
# Run the logic suite on localnet
bun run test:localRequirements: Solana CLI 3.1+ (tested on 4.0.1), Anchor 1.1.2, surfpool 1.4+, Bun, Rust 1.89.0
TypeScript integration tests use @anchor-lang/core with artifacts from anchor build (target/idl/opal.json, target/types/opal.ts).
Privy social login + embedded Solana wallet (devnet). Assertion UI uses mock data until on-chain txs are wired:
cd web && cp .env.local.example .env.local && bun run check-env && bun run devSee web/README.md and Privy setup.
- Web frontend — Privy auth, devnet wallet, mock assertion flows
- Privy setup — dashboard checklist
- Architecture — account model, state machine, instruction flow
- Resolution — how statements move from assertion to final outcome
- Tokenomics — bonds, slashing, rewards, fees
- Glossary — shared vocabulary
- Decision records — the rationale behind the locked design
Tests are TypeScript integration tests run with @anchor-lang/core; there are no Rust unit tests. Targets split by environment (ADR-0006):
# [Built] Logic suite on surfpool (Anchor v1's default local runner)
bun run test:local
# [MVP-target] Real integration against devnet, where the resolver service
# and MagicBlock ER actually exist: anchor test --skip-local-validator
anchor testbun run test:local is an alias for anchor test today. Pointing anchor test at devnet — a funded devnet keypair, a deployed program, and --skip-local-validator — lands in the test-split follow-up PR per ADR-0006; until then it still spins up a local surfpool network.
The deadline/window tests depend on [surfpool] block_production_mode = "clock" in Anchor.toml: it advances slots on a timer so the on-chain Clock moves during the suite's real-time sleeps. Without it, surfpool only ticks the clock when a transaction lands, and every timing test fails.
Current coverage: happy paths (undisputed, LLM resolution, full escalation), config validation, deadline violations, state guards, account mismatches, token balance assertions.
The optimistic core is built: the account model, the six-state machine (Asserted → PendingLLM → AssertedLLM → PendingVote → Voting → Resolved), and the optimistic-resolution plus dispute plumbing all exist on-chain today. Integration tests drive the LLM round through submit_llm_resolution signed with a test resolver keypair.
These pieces are v1 targets, not yet shipped:
- Trusted LLM resolver — the on-chain half is built:
submit_llm_resolutionis gated on a dedicatedProtocolConfig.resolverkey (separate fromauthority, so a leaked hot resolver key can only post a challengeable verdict) and accepts onlyTrue/False/Unresolvable. The off-chain service that makes the real LLM call and posts verdicts is the remaining target; the former 3-feed Switchboard council was removed per ADR-0002. On-chain LLM provenance (prompt/response/evidence hashing) is deferred to a[Vision]trust-minimized resolver. - Private staked voting — opening a vote sets up the round, but real MagicBlock private voting (delegation, ER settlement) is the MVP target.
- Resolution Spec on Arweave — the on-chain
auxiliary_hashfield exists; off-chain Arweave storage and integrity-checking is planned. - No-fault settlement & reward split —
Unresolvableno-fault settlement and the share-based settlement split (llm_disputer_reward_share_bps,vote_disputer_reward_share_bps,voter_reward_share_bps,treasury_share_bps) are planned; onlyprotocol_fee_bpsis applied today. - Field names — state and config fields still carry the legacy
pusdprefix; a later PR renames them tousdcto match the committed asset.
Directions recorded so they're not mistaken for current behavior:
- OPAL token — governance, reputation/staking, voter incentives. Dropped from the MVP; staked USDC and the
authoritykeypair cover its jobs. - Trust-minimized LLM — Switchboard On-Demand or TEE-attested (permissionless) inference replacing the trusted resolver; on-chain LLM provenance hashing, if any, lands here.
- Proof-of-personhood — enabling sub-linear/quadratic voting weight without Sybil collapse.
- Stake-duration reputation — long-term staking that accrues voter weight.
- Timed resolution — assertions carrying a resolves-at date so they can't finalize before the truth exists.
Not audited. Don't use in production.
Not selected yet.