The trusted LLM resolver service for Opal — the off-chain half of ADR-0002.
A single long-running Bun worker that:
- Scans the Opal program for unresolved
LlmResolutionRoundPDAs (getProgramAccounts, filtered on the account discriminator + outcome byte255at offset 72; account size 98 bytes). Chain state is the work queue — there is no database. - Reads the disputed assertion's statement and calls Gemini to judge it against its Resolution Spec. Google-Search grounding (live web) is optional via
GEMINI_GROUNDING— off by default because it requires a paid Gemini tier. - Submits the verdict on-chain via
submit_llm_resolution, signed by the resolver keypair (ProtocolConfig.resolver). Verdicts areTrue (0),False (1), orUnresolvable (3)— the program rejectsTooEarly (2)per ADR-0005.
Idempotency is on-chain: a posted verdict drops out of the next scan, and the program's PendingLLM guard rejects any second submission. Chain state is both the queue and the dedup.
bun install
cp .env.example .env # fill in GEMINI_API_KEY, RPC_URL, PROGRAM_ID, RESOLVER_KEYPAIR_PATH
solana-keygen new -o keys/resolver.keypair.json # resolver signing key; must match ProtocolConfig.resolver on-chain
bun startGEMINI_GROUNDING=false (the default) resolves from the model's training knowledge with no live web access; set it true only on a paid Gemini tier (grounded calls 429 on the free tier).
Flags:
--manual <assertion_pubkey> <outcome_code>— post one verdict by hand and exit (demo fallback).--once— single scan pass instead of the 10s loop.
bun run test # unit: verdict parsing, injection screening, discriminators (no external deps)
bun run e2e # end-to-end against the dummy anchor program on a local validatorbun run test needs nothing external. The e2e runs against test-program/ — a minimal Anchor 1.1.2 program with the same 98-byte round layout as Opal, so the full scan → resolve → submit loop is proven without touching the real program. It requires the Solana CLI (solana-test-validator) and Anchor 1.1.2, and the dummy program built first:
cd test-program && anchor build && cd ..
bun run e2eNote: plain
bun test(no script) runs both suites, so it carries the same e2e prerequisites. Usebun run testfor the dependency-free unit suite.
For a demo or a one-off, just run it locally — bun start (see Run); no container needed.
For persistent operation it needs to stay up whenever a dispute exists (an unresolved assertion has no on-chain timeout), so long-term it runs as a single always-on worker. The Dockerfile packages exactly that — no HTTP surface, two secrets (resolver keypair + Gemini key) injected via env:
docker build -t opal-resolver .
docker run --env-file .env opal-resolverAny container host works (Fly.io, Railway, a small VPS); restart-on-crash comes from the host's restart policy.
- The resolver key is a hot key. On-chain it can only post challengeable verdicts — it is deliberately not the protocol authority.
- Statements are untrusted input: they are wrapped in tags, screened for prompt injection, and the model is instructed to return
Unresolvableon any attempt to steer it. - Never commit
.envor anything underkeys/.