Mechanize the verdict.
Accord is a general-purpose, capital-weighted Schelling arbitration oracle on Solana — an on-chain dispute-resolution primitive inspired by Kleros. Any Solana program (the Arbitrable) files a subjective Dispute via two CPI calls; the Accord draws stake-weighted Jurors (VRF), collects commit-reveal votes, and emits a Ruling governed by game-theoretic incentives (an honest-stake-majority assumption) instead of a hired-judge committee.
[!IMPORTANT] > Accord is an arbitration oracle, not a self-enforcing decentralized court. Several roles hold privileged or concentrated power (Subaccord authority, upgrade multisig, VRF provider, indexer, cranker, large stakeholders, evidence operator). The honest Trust Profile states every residual assumption and the security-value ceiling. Read it before securing real value. This is pre-mainnet, unaudited software.
It is a standalone, reusable product: the Accord has no knowledge of the filing program's domain. Dispute resolution becomes composable infrastructure.
your program ──create_dispute()──► Accord ──draws jurors, runs commit/reveal──► Ruling
▲ │
└────────────────────────────get_ruling()────────────────────────────────────┘
- Schelling Point = honesty (honest-majority-stake assumed). Jurors converge on the truthful answer because voting coherently with the majority is the profitable strategy. No central judge is picked — but see the Trust Profile: Schelling honesty holds conditional on an honest stake majority.
- Party-agnostic Arbitrable interface. Integrate with two CPI calls:
create_dispute()→get_ruling(). The Accord never learns your domain. - Permissionless Subaccords. Specialized Juror pools (automotive, freelancing, NFTs, …). Anyone can register one; each defines its own staking token, min stake, windows, and slash factor.
- Per-Subaccord staking token. Each pool picks the SPL token Jurors stake (USDC by default). Stake is the anti-sybil mechanism and the coherence-slashing substrate.
- Verifiable sortition. Stake-weighted Juror draw over a live on-chain
stake accumulator (a Merkle-Sum Tree maintained on every
stake/unstake), seeded by a committed VRF. The root is canonical by construction — no posted root, no bond, no challenge window — so the draw is manipulation-resistant by mechanism, not by fraud-proof (ADR-0012; supersedes ADR-0003/0008/0009). - Commit-reveal + exponential appeals. Secret votes prevent vote-copying so the Schelling Point forms independently; each appeal doubles the panel + 1 (3 → 7 → 15 → 31), making bribery more expensive (deterred, not impossible — see the security-value ceiling).
[!IMPORTANT] > Project status. The on-chain program (
programs/accord) implements the full v1 instruction set with a LiteSVM unit-test per instruction. The TypeScript SDK (packages/sdk) and the jest/ Surfpool integration suite (tests/) are scaffolded and under active development (Codama codegen — ADR-0010). See Project Status. This is pre-mainnet, unaudited software — do not secure real value with it yet.
- Tech Stack
- Prerequisites
- Getting Started
- Architecture
- The Arbitrable Interface
- Environment Variables
- Available Commands
- Testing
- Project Status
- Deployment
- Troubleshooting
- Contributing
- License
- Further Reading
- Program language: Rust (Anchor framework)
- Framework: Anchor
1.0.2 - Runtime: Solana
3.1.10(BPF; host Rust viarust-toolchain.toml= stable) - Randomness: Magicblock / Solana VRF (
ephemeral-vrf-sdk 0.4.1) - Token layer: SPL Token + Associated Token (
anchor-spl 1.0.2) - SDK: TypeScript (
@solana/web3.js,@anchor-lang/core) — Codama + Solana Kit codegen pipeline (ADR-0010, in progress) - Docs: MkDocs Material (
apps/docs/) - Package manager: pnpm
9.15.0(workspaces) + Cargo (Rust workspace) - Lint/format:
rustfmt,clippy,tsc --noEmit, Prettier, ESLint, markdownlint, gitleaks (via pre-commit)
- Rust (stable) —
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - Node.js 20+ and pnpm 9.x —
corepack enable && corepack prepare pnpm@9.15.0 --activate - Solana CLI + Anchor — installed automatically by
make prep(below) - Poetry (only for the docs site) —
curl -sSL https://install.python-poetry.dev | python3 -
[!TIP] >
make prepinstalls Solana3.1.10(viasolana-install) and Anchor1.0.2(viaavm) for you — you do not need to pin them manually.
git clone https://github.com/xeroc/accord.git
cd accordmake prepThis runs solana-install init 3.1.10, installs Anchor 1.0.2 through avm,
and runs pnpm install across the workspace. Re-run only when toolchain
versions change.
make buildmake build runs anchor build (compiles programs/accord to
target/deploy/accord.so and emits the IDL) followed by pnpm -r run build
(the SDK and any apps). The first build downloads and compiles the Solana BPF
toolchain — expect a few minutes.
The provider defaults to localnet with ~/.config/solana/id.json
(see Anchor.toml). Generate a keypair if you don't have one:
solana-keygen newSwitch clusters with the Solana CLI:
solana config set --url localhost # local validator / Surfpool
solana config set --url devnet # devnetFast in-process unit tests (no validator needed):
make test_unitFull end-to-end suite against a live validator (start Surfpool first):
make run_surfpool # in a separate terminal
make test_surfpoolSee Testing for the two-harness philosophy.
.
├── programs/
│ └── accord/ # The on-chain arbitration program (Anchor)
│ ├── src/
│ │ ├── lib.rs # #[program] instructions + account contexts
│ │ ├── state.rs # Account structs, enums, PDA proof types
│ │ ├── constants.rs # Size bounds, windows, PDA seed prefixes
│ │ ├── errors.rs # AccordError codes
│ │ └── events.rs # Emitted events for off-chain indexers
│ ├── tests/ # LiteSVM unit tests (one file per instruction)
│ ├── accord.qedspec # Formal-verification spec (qedgen)
│ ├── SPEC.md # v1 build spec (account model, state machine)
│ └── security-checklist.md
├── packages/
│ └── sdk/ # @useaccord/sdk — TypeScript SDK (codegen, in progress)
├── tests/ # jest + Surfpool integration suite
├── apps/
│ └── docs/ # MkDocs Material docs site (domain TBD)
│ ├── docs/
│ │ ├── adr/ # Architecture Decision Records (0001–0012)
│ │ ├── integration/ # Arbitrable integration guide
│ │ ├── reference/ # Accounts, instructions, state machine, errors
│ │ └── security/ # Accumulator trust, sortition/VRF, circuit breaker
│ └── mkdocs.yml
├── formal_verification/ # Lean / qedgen harness
├── CONTEXT.md # Domain language (ubiquitous-language glossary)
├── PROJECT.md # Project rationale (the "why")
├── BRAND.md # Brand model
├── Cargo.toml # Rust workspace
├── Anchor.toml # Anchor workspace + provider + test script
├── Makefile # Build / test / lint orchestration
├── pnpm-workspace.yaml # TS workspace globs (apps/*, packages/*, tests)
└── tsconfig.base.json # Shared TS compiler options
Note
The root package.json intentionally has no scripts block. The Makefile
orchestrates builds; lint/test fan out via pnpm's recursive filter. Don't add
root scripts — they'd duplicate the Makefile.
The dispute lifecycle is a state machine advanced by permissionless cranks (anyone can move it forward when a window elapses):
stateDiagram-v2
[*] --> Created: create_dispute (Arbitrable CPI)
Created --> Drawn: request_vrf → commit_vrf_callback (freezes root) → draw_seat × N
Drawn --> Committed: commit (hash(vote, salt, juror))
Committed --> Revealed: reveal ({vote, salt})
Revealed --> RoundResolved: finalize_round (tally)
RoundResolved --> Final: finalize_dispute (no appeal / max reached)
RoundResolved --> Drawn: appeal → new round (2N+1 jurors, same frozen root)
Final --> [*]: get_ruling (lazy read by Arbitrable)
Odd Juror counts (3 / 7 / 15 / 31) make ties impossible.
Every account stores its canonical bump so handlers reuse the same PDA
without re-deriving. Large accounts (Round) are #[zero_copy] (AccountLoader)
to fit BPF's stack.
| Account | Seeds | Purpose |
|---|---|---|
Subaccord |
["subaccord", creator, risk_type] |
A specialized Juror pool: staking token, windows, alpha, authority. Holds the stake accumulator root (root_hash, total_stake, next_index, depth) |
JurorStake |
["stake", subaccord, juror] |
A Juror's staked capital + active_draws lock count + tree_index (leaf position, assigned at first stake) |
Dispute |
["dispute", filer, nonce] |
A case: options, evidence hash, state, final_ruling, committed_vrf, frozen_root (set at VRF-commit) |
Round |
["round", dispute, round_idx] |
Per-round jurors, commits, reveals, result (zero-copy) |
AppealBond |
["bond", dispute, round_idx] |
Custody record for one appeal bond |
PendingUpdate |
["update", subaccord, nonce] |
Timelocked Subaccord parameter update (48h) |
PauseState |
["pause"] |
Singleton program-level circuit breaker |
| token vaults | Subaccord-PDA-owned SPL accounts | Stake pool + fee pool |
The draw is the security-critical path (ADR-0012; supersedes ADR-0003/0008/0009):
- Live stake accumulator. The Subaccord's Juror set + stake weights are kept
as an on-chain Merkle-Sum Tree root (
root_hash,total_stake,next_index,depth), maintained incrementally on everystake/unstake. The caller supplies the juror's leaf path; the chain verifies it against the stored root, reads the liveJurorStake.amount, applies the verified vault delta, and recomputes the root (O(log N)). The full tree lives off-chain (indexers) but any auditor can rebuild the root fromJurorStakeviagetProgramAccountsand check it on-chain. The root is canonical by construction — there is no posted root to withhold or fabricate. - VRF + frozen root.
request_vrfasks the VRF oracle for randomness; the oracle's identity-signedcommit_vrf_callbacklands the result and freezesdispute.frozen_root = subaccord.root. Freezing when randomness becomes known (not at filing) keeps capital fully live until the draw and closes the manipulation window. One VRF + one frozen root serve the whole dispute; appeals draw a larger panel from the same fixed pool. - Per-seat draw. A permissionless cranker submits each seat's membership
proof in its own
draw_seattx (the 1232-byte packet can't hold N proofs). The program verifies each proof againstfrozen_root, checks the sortition criterion (prefix ≤ r_i < prefix + stake, prefix derived from authenticated sibling sums), enforces the inflation guard (JurorStake.amount ≥ leaf.stake), and samples without replacement. Nodraw_attemptgrind, no collision liveness stall.
Inherited from Kleros (live since 2019, 1000+ disputes):
- Fee: filer pays
N · fee_per_juror; appellant paysN_new · fee_per_juror+ bond. - Slash: each Incoherent Juror loses
α · min_stake(flat; ADR-0003). - Redistribution: forfeited fees + slashed stake → Coherent Jurors, equal split.
- Non-reveal penalty: ≥ the Incoherent penalty (forces reveal).
- Appeal bond: forfeited to Coherent Jurors of the final round if the appeal does not flip the prior Ruling; returned if it flips.
- Cross-round settlement: every round is re-settled against the final Ruling.
The Accord stores only an evidence hash on-chain (ADR-0006). A Subaccord-designated Evidence Operator re-encrypts the evidence for the drawn Jurors off-chain:
claimant ──encrypt(evidence, operator_pubkey)──► encrypted blob ──► off-chain store
on-chain Accord: evidence_hash only
dispute filed + Jurors drawn
▼
evidence_operator service: decrypt → re-encrypt per drawn Juror (+ optional watermark)
▼
Juror decrypts, verifies cleartext vs on-chain evidence_hash
Your program integrates with two CPI calls. The Accord handles everything else.
// 1. File the dispute
let dispute = accord::create_dispute(
ctx.accounts.clone(),
vec![option_a_hash, option_b_hash], // 2+ option hashes
evidence_hash, // commitment to the evidence
nonce, // caller-chosen, for PDA uniqueness
fee, // INITIAL_NUM_JURORS (3) * fee_per_juror
)?;
// 2. Read the ruling (lazy — call whenever, after finalization)
let ruling: Option<u8> = accord::get_ruling(ctx.accounts.dispute)?;import { Accord } from "@useaccord/sdk";
// File a dispute
const { dispute } = await accord.createDispute({
subaccord: subaccordAddress,
options: [hashOption("Yes"), hashOption("No")],
evidenceHash: evidenceCommitment,
nonce: 1n,
fee: requiredFee,
});
// Later: read the ruling (0 = option A, 1 = option B, null = not final)
const ruling = await accord.getRuling(dispute);Note
The full instruction surface (24 instructions) is documented in the
Protocol Reference and
programs/accord/SPEC.md. Integrators normally only need create_dispute
and get_ruling; the rest are permissionless cranks.
The program is configured on-chain (per-Subaccord params), not via env vars. Local development needs only Solana CLI config:
| Variable | Description | Example |
|---|---|---|
ANCHOR_WALLET |
Path to the provider keypair (defaults to Solana CLI config) | ~/.config/solana/id.json |
RPC_URL |
Cluster RPC endpoint (or use solana config set --url) |
localhost:8899 |
Anchor.toml pins the provider:
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"All orchestration lives in the root Makefile. The root package.json has no
scripts by design.
| Command | Description |
|---|---|
make prep |
Install Solana 3.1.10 + Anchor 1.0.2 (via avm), then pnpm install |
make build |
anchor build (programs) then pnpm -r run build (packages/apps) |
make test |
Rust unit tests + jest suite against a local validator (anchor test) |
make test_unit |
LiteSVM Rust unit/TDD tests (fast, no validator) |
make run_surfpool |
Start a Surfpool local fork (separate terminal) |
make test_surfpool |
Full suite against a running Surfpool instance |
make lint |
Lint every workspace that declares a lint script |
make clean |
Remove build artifacts and node_modules |
cd programs/accord && cargo test |
Rust unit tests in isolation |
cd packages/sdk && pnpm run build |
Build the SDK |
cd tests && npx jest -t "<name>" |
Run a single integration test by name |
cd apps/docs && poetry run mkdocs serve |
Serve the docs site locally (localhost:8000) |
Per-package lint auto-fix (where defined):
pnpm --filter @useaccord/sdk run lint:fixThe project uses two complementary harnesses (decision veridao-8ys4):
- Location:
programs/accord/tests/*_litesvm.rs - Run:
make test_unit - What it is:
anchor-litesvm0.4.xruns the real compiled.soin-process — no validator. One freshAnchorLiteSVMcontext per test. Each instruction has a test file covering happy-path, authority, reinit guard, timelock, arithmetic, and closure cases.
- Location:
tests/*.spec.ts - Run:
make run_surfpool(start the fork), thenmake test_surfpool - What it is: the real validator behaviour — CPI chains, VRF, token
transfers. Long-running (
testTimeout: 120000).
Every feature/instruction follows RED → GREEN → REFACTOR. The failing test
ships first; no exceptions. A milestone is completed only when all its leaf
tests are green.
[!IMPORTANT] > The
no-entrypointfeature quirk. The program'sentrypoint!symbol collides with a builtin when the crate is statically linked into the test binary. Rust tests therefore buildaccordwith--features no-entrypoint(types only). The.so— built separately viacargo build-sbf/anchor buildwith the entrypoint — is what LiteSVM loads. All*_litesvm.rsfiles are gated with#![cfg(feature = "no-entrypoint")]soanchor build(which doesn't pass the feature) skips them during IDL gen.make test_unithandles both steps.
| Component | Status | Notes |
|---|---|---|
programs/accord (on-chain) |
✅ Implemented | Full v1 instruction set + per-instruction LiteSVM tests |
Formal verification (accord.qedspec) |
Four economic invariants modeled; pending VRF/param-bounds binding | |
@useaccord/sdk (TypeScript) |
🚧 Scaffolded | Codama codegen pipeline in progress (ADR-0010); facade stub only |
tests/ (jest/Surfpool) |
🚧 Scaffolded | Harness configured; integration specs in progress |
apps/docs (MkDocs) |
✅ Live | Full integration guide, protocol reference, security docs, ADRs |
| Security audit | ❌ Not started | Pre-mainnet; do not secure real value yet |
The program ID is set in declare_id! (programs/accord/src/lib.rs) and
mirrored in Anchor.toml. The canonical deploy keypair is provisioned by the
operator (see AGENTS.md §Gotchas); until then, local builds use
--ignore-keys to skip the keypair check.
# Devnet
solana config set --url devnet
anchor build --ignore-keys
anchor deploy --provider.cluster devnet
# Verify the deployed program
solana program show <PROGRAM_ID>The upgrade authority is a Squads multisig at launch; after a sufficient
audit it is set to None (frozen, immutable). The on-chain PauseState
singleton (seeds ["pause"]) is a separate circuit breaker: pause() is
instant and authority-gated; unpause() is timelocked
(propose_unpause → execute_unpause after UNPAUSE_TIMELOCK_SLOTS) so a
freeze is always recoverable on a known schedule. While paused, create_dispute
/ stake / appeal revert; in-flight disputes resolve normally.
Bundle the pause-singleton init with deploy (front-running is an ops concern):
# initialize_pause — the caller becomes the pause authority (the Squads multisig).
# Invoke it once, ideally bundled with the deploy tx (front-running is an ops
# concern). There is no Makefile target yet — call the instruction directly via
# the SDK / a small script, e.g.:
# accord.methods.initializePause().accounts({...}).rpc()Cause: Solana CLI < 3.x bundles platform-tools v1.48 / cargo 1.84, which
can't parse edition2024 manifests.
Fix: make prep installs Solana 3.1.10, which drops the flag. If you
must invoke cargo build-sbf directly on an older CLI, pass
--tools-version v1.52. (anchor build manages its own toolchain and is
unaffected.)
The .so must be built before the unit tests load it. make test_unit
does both; if you run cargo test directly, build first:
cargo build-sbf --manifest-path programs/accord/Cargo.toml
cargo test --manifest-path programs/accord/Cargo.toml --features no-entrypointanchor build without --ignore-keys fails when the gitignored worktree
keypair doesn't match declare_id!. This is expected — the worktree keypair is
throwaway. Use --ignore-keys (all Makefile targets already do):
anchor build --ignore-keysNever run anchor keys sync without the canonical keypair — it would
rewrite declare_id! to adopt the random worktree key, desyncing the SDK,
tests, and Codama client. The canonical keypair is provisioned by the operator
(see AGENTS.md §Gotchas).
On Anchor 1.0.2 + Solana 3.x deps, IDL generation is unblocked end-to-end
via the idl-build feature (see programs/accord/Cargo.toml). If you hit an
older Anchor, ensure the crate declares idl-build in [features].
Integration tests need a running validator. Start Surfpool first:
make run_surfpool # keeps running; use a separate terminal
make test_surfpoolEnsure the Solana BPF toolchain and system libs are present. make prep
handles the Solana side; for host crates you need a working rustc (stable)
and standard build essentials (build-essential / Xcode CLT).
- Read first:
CONTEXT.md(domain language) → this README →apps/docs/adr/(the why behind every locked decision). - TDD only. Write the failing test first, then implement to pass.
- Lint is law. Run
make lint(and the relevant test) before committing. Pre-commit hooks (fmt,cargo-check,markdownlint,gitleaks,detect-private-key) run automatically. - Track work with beans. This repo uses the
beansCLI for issue tracking. Checkbeans list --json --readybefore assuming docs reflect reality — active milestones may supersede code state. Include relevant bean IDs in commit messages (the bean prefix isaccord-per.beans.yml). - ADRs are immutable once deployed. A superseded decision gets a new ADR that references the old one.
Install the git hooks:
pip install pre-commit
pre-commit installUNLICENSED (private) — see package.json. The program crate
(programs/accord) ships under the workspace license.
- Docs site: docs (domain TBD) — Quickstart, Integration Guide, Protocol Reference, Security, ADRs
- Trust Profile — who holds power, what's trusted, the security-value ceiling
CONTEXT.md— domain language / ubiquitous-language glossaryPROJECT.md— project rationale (the "why")BRAND.md— brand modelprograms/accord/SPEC.md— v1 build spec (account model, state machine, economics, edge cases)programs/accord/security-checklist.md— security audit authority (findings citefile:line)- ADRs (
apps/docs/adr/):- 0001 Schelling-point Accord replaces hired-judge committee
- 0002 Per-Subaccord staking token, no Accord token in v1
- 0003 Draw — Merkle snapshot, off-chain sortition, distinct Jurors (partially superseded by 0012)
- 0004 Party-agnostic; appeal is permissionless
- 0005 Subaccord authority — pubkey, 48h timelock
- 0006 Evidence — on-chain hash, trusted re-encryption operator
- 0007 Upgrade authority — Squads multisig, then freeze
- 0008 Snapshot trust hardening — anchor-slot, fraud predicates, sortition (partially superseded by 0012)
- 0009 Stake-weighted verifiable sortition — MST, committed VRF (partially superseded by 0012)
- 0010 SDK — Codama codegen + Solana Kit facade
- 0011 Evidence Operator Daemon — off-chain decrypt-re-encryption service
- 0012 On-chain stake accumulator replaces the optimistic snapshot (current draw mechanism)
ACCORD
An accord, not a committee.