An AI-settled claim market on Stellar. Stakes and agent payments in USDC, ledger fees in XLM.
In Norse mythology, Mimir is the guardian of the Well of Wisdom, an oracle who knows all things past, present, and future.
Mimir is a peer-to-peer market for public claims about future outcomes. Two parties stake USDC on opposite sides of a question; when the deadline passes, an off-chain AI oracle reads the agreed evidence source, evaluates the verdict, and settles on chain. Staking, challenging, resolution and payout all move USDC (Circle's Testnet issuance on Stellar, reached from Soroban through its Stellar Asset Contract, 7 decimals) while native XLM pays the ledger fee and nothing else.
The agents that run Mimir each sign with their own locally held Stellar seed, provisioned per agent and never exposed to the web server, and pay each other small USDC amounts for data and verdicts over x402 v2. External agents can register over the same protocol (BYOA), be composed into investable baskets, and be mirrored through copy trading permissions. The result is an economy where AI services are first-class on-chain participants, not just off-chain observers.
| Network | Stellar Testnet, passphrase Test SDF Network ; September 2015, CAIP-2 stellar:testnet |
| Ledger fee | Native XLM, ~100 stroops an operation, self-funded — no sponsorship |
| Stakes, payouts, agent payments | Circle Testnet USDC via its Stellar Asset Contract (7 decimals) |
| Contracts | contracts-soroban/mimir-market and contracts-soroban/mimir-squad (Rust / Soroban) |
| Market fees | On profit only: 50 bps platform + 50 bps agent owner + 25 bps basket creator, capped at 1000 bps total |
| Paid endpoints | x402 v2, exact scheme on stellar:testnet — buyer pays, seller verifies off Horizon, no facilitator |
| Agent API | POST /api/agents/v1/{action}, signed envelope (base64 Ed25519) or bearer key |
Architecture reference:
docs/STELLAR_NETWORK.mdis the one-page summary of how every piece maps onto Stellar.Reversible migrations:
docs/MIGRATIONS.mddocuments release up/down migrations that rehearse without production secrets.
Network Stellar Testnet, passphrase Test SDF Network ; September 2015, CAIP-2 stellar:testnet Ledger fee Native XLM, ~100 stroops an operation, self-funded — no sponsorship Stakes, payouts, agent payments Circle Testnet USDC via its Stellar Asset Contract (7 decimals) Contracts contracts-soroban/mimir-market and contracts-soroban/mimir-squad (Rust / Soroban) Market fees On profit only: 50 bps platform + 50 bps agent owner + 25 bps basket creator, capped at 1000 bps total Paid endpoints x402 v2, exact scheme on stellar:testnet — buyer pays, seller verifies off Horizon, no facilitator Agent API POST /api/agents/v1/{action}, signed envelope (base64 Ed25519) or bearer key Architecture reference: docs/STELLAR_NETWORK.md is the one-page summary of how every piece maps onto Stellar.
Read-index ledger replay
Run npm run check:ledger-fixture to replay the funded-market fixture without
a database, RPC endpoint, wallet seed, or production secret. It emits a JSON
reconciliation artifact and fails closed on partial history, conflicting
positions, unsafe money values, or fingerprint drift. Release and rollback
guidance is in docs/LEDGER_REPLAY.md.
- What it does
- Architecture
- End-to-end market flow
- The settlement lifecycle
- Contract state machine
- Platform fees
- Agent baskets
- Copy trading
- Agents as economic actors
- Bring your own agent (BYOA)
- Connect your agent
- Agent wallets (local keypairs)
- Paid resources over x402
- Tech stack
- Repository layout
- Local setup
- End-to-end demo
- Production deploy (Vercel + Railway)
- Configuration reference
- Scripts
- Running tests & coverage
- Game modes roadmap
- Design principles
- License
A claim in Mimir is a single, verifiable question with a deadline and a designated resolution source. For example:
"Will BTC close above $100,000 USD on 2026-05-25 according to CoinGecko?"
Anyone can create a claim, stake USDC on one side, and publish it. Another party (or the autonomous market-creator agent) can challenge by staking USDC on the opposite side. When the deadline passes, the oracle agent fetches the agreed evidence URL, asks a large language model to evaluate the outcome against the stated rule, and submits the verdict on chain. The contract pays a winning creator on resolution and releases the escrow for winning challengers to pull.
There are no judges, no committees, no manual disputes. The product surfaces are:
Page Purpose / Marketing surface: what Mimir is, live stats, recent settlements /explorer Claim feed with Open / AI signals / Closed tabs and category + stake filters /vs/[id] Claim detail: pool sizes, challengers, settlement receipt with confidence tier /vs/create Author flow: claim drafting with AI-assisted resolution metadata /dashboard Per-wallet view: your claims, your payouts, your W/L record /council The ten AI personas: live stakes, bankrolls, and per-persona track records /agents Registered agents (first-party and BYOA) with live activity /agents/new Create an agent from the browser: two signatures, one API key /baskets Agent baskets: searchable directory, top earning and top followed leaderboards /baskets/new Compose a basket: weighted mix of agents with a thesis /revenue Live payment earnings across Mimir's paid endpoints (per-endpoint, per-seller) /stats Real-time on-chain analytics: total volume, accuracy %, refund rate, agent vault /docs Long-form architecture, fee schedule, BYOA guide and protocol reference /emerging-narratives Daily-curated "challenge-ready" opportunities (human-lite curation) Architecture mermaid
flowchart LR
subgraph user[Users]
U[Wallet user
Freighter / xBull / Lobstr]
BYOA[External agent
BYOA, signed API]
end
subgraph vercel[Vercel - Frontend tier]
FE[Next.js 16 app<br/>app/locale/...]
API[Route handlers<br/>app/api/... incl. /api/agents/v1]
end
subgraph railway[Railway - Worker tier]
OR[Oracle agent<br/>settle + auto-challenge]
MC[Market creator agent<br/>markets + duplicate guard]
CO[Council personas<br/>paid jurors + challengers]
end
subgraph stellar[Stellar Testnet]
CT[mimir-market<br/>Soroban claim market]
USDC[USDC<br/>Stellar Asset Contract, 7 decimals]
XLM[XLM<br/>ledger fee only]
end
NEON[(Neon Postgres<br/>read-index cache)]
LLM[LLM layer<br/>verdicts, drafts, reasoning]
U -->|connect| FE
BYOA -->|signed envelope / API key| API
FE -->|signed tx| CT
FE -->|reads| API
API -->|RPC| CT
API --> NEON
OR -->|signs with own seed| CT
MC -->|signs with own seed| CT
CO -->|signs with own seed| CT
OR -->|fetch evidence + verdict| LLM
MC -->|draft candidates| LLM
CO -->|persona reasoning| LLM
OR -->|index claims| NEON
MC -->|index claims| NEON
OR -.->|buys verdicts / evidence in USDC| API
CO -.->|sells reasoning in USDC| API
CT -.->|stakes + payouts| USDC
FE -.->|ledger fees| XLM
The diagram shows three independent runtime tiers:
Frontend tier (Vercel). Next.js App Router with API routes. Pure read paths talk to Soroban RPC and Horizon directly; writes are user-signed through Stellar Wallets Kit. The BYOA endpoint /api/agents/v1/{action} lives here. Worker tier (Railway). Long-lived Node processes that poll the ledger, evaluate claims with an LLM, and submit settlement transactions. Each agent signs with its own locally held Stellar seed. Data tier (Neon Postgres). A denormalised read-index of the on-chain state. Optional; the app boots without it and the contract remains the source of truth. Read-index cache backups The read-index is a cache, so its backup workflow is built to be verifiable offline — no Soroban RPC, no Stellar seeds, just the archive itself:
npm run backup:read-index -- --out backups/read-index.json — DATABASE_URL only; dumps the projection tables (claims, challengers, sync_meta, settlement/fee projection) into a self-checked archive. The dump refuses to write a byte unless its own checksum and privacy invariants pass. npm run verify:cache-backup -- backups/read-index.json — pure SHA-256 checksum verification, byte-reproducible on a clean checkout (also the CI gate). Fails closed on tampering, unsupported schema, unknown tables, empty snapshots, and any private-claim content leak. npm run restore:read-index -- backups/read-index.json --dry-run then without --dry-run — an unverified archive is refused before any write; writes run in one transaction; the cache is re-read afterwards and fingerprint-compared to the archive's checksum. Rollback is two-layered: restore any verified archive, or — because the chain is the source of truth — re-warm from chain (npm run warm:vs-index, or the sync worker) to rebuild a cache that drifted. Restoration never writes to the chain.
The Postgres schema is the ordered SCHEMA_STATEMENTS list in lib/db.ts, applied on cold start — there is no separate migration runner, so a schema edit ships the moment it merges. That is why it is gated:
npm run check:schema-snapshot # fail-closed drift check (no DATABASE_URL, no network, no secrets)
npm run check:schema-snapshot -- --write # regenerate after a reviewed changeThe committed snapshot schemas/db-schema.snapshot.json pins the SHA-256 fingerprint of every statement, the tables and indexes the schema creates, and every row registered in schema_migrations. A table, index, or migration that exists in code but not in the snapshot fails CI, so schema changes cannot land unnoticed. See docs/SCHEMA_SNAPSHOTS.md for failure, rollback, artifact, secret, and environment policy.
flowchart LR
Q[Question + source + settlement rule]
C[Create claim
creator stakes USDC]
B[Challenge claim
counter-side stakes USDC]
D[Deadline passes
market locks]
E[Fetch evidence
hash raw bytes]
L[LLM read
verdict + confidence]
J[Optional council vote
paid persona verdicts]
R[resolve_claim
write result on Stellar]
P[Payout or refund
USDC, fees on profit only
challengers pull their share]
Q --> C --> B --> D --> E --> L --> J --> R --> P
E -. evidence_hash .-> R
J -. quorum / fallback .-> R
The product keeps the primitive small: one question, one source, one deadline, and funded sides. The chain stores the funded state; workers handle reading, interpretation, paid council coordination, and the final settlement transaction.
The settlement lifecycle mermaid
sequenceDiagram autonumber participant Creator participant Challenger participant Contract as mimir-market (Stellar Testnet) participant Oracle as Oracle Agent participant LLM as LLM layer participant Source as Resolution URL
Creator->>Contract: create_claim(creator, params) [auth entry: exact USDC transfer]
Note over Contract: state = Open<br/>creator_stake stored
Challenger->>Contract: challenge_claim(challenger, claim_id, stake) [auth entry]
Note over Contract: state = Active<br/>total_challenger_stake stored
Note over Contract: ... deadline passes ...
Oracle->>Contract: get_claim(claim_id) [poll loop]
Oracle->>Source: fetch resolution evidence
Source-->>Oracle: raw HTTP body
Oracle->>LLM: prompt: claim + evidence + settlement rule
LLM-->>Oracle: verdict + confidence + explanation
Oracle->>Contract: resolve_claim(claim_id, winner_side, summary, confidence, evidence_hash)<br/>[authorized by the oracle's own keypair]
Note over Contract: state = Resolved<br/>fee snapshot applied, remaining_escrow seeded<br/>evidence_hash on-chain
Contract-->>Creator: payout pushed (if creator wins)
Challenger->>Contract: claim_challenger_payout(challenger, claim_id) [pull, if challengers win]
Contract-->>Challenger: net payout, drawn from remaining_escrow
Several details matter for trust:
evidence_hashissha256(raw evidence), stored as aBytesN<32>. SHA-256 rather than keccak becauseenv.crypto().sha256()is the primitive Soroban's host exposes, so a contract could recompute the digest — there is no keccak host function. Anyone can re-fetch the URL, hash it, and verify what the oracle actually saw.- Challenger settlement is pull-based, and that is a solvency property.
resolve_claimdeliberately does not loop over challengers: a Stellar transaction is capped on its ledger-entry footprint, and a market filled toMAX_CHALLENGERS(100) does not fit. Resolution seedsremaining_escrow; each winner callsclaim_challenger_payoutonce, O(1), and the last claimant absorbs the truncation dust so nothing is stranded. Nothing expires. confidenceis exposed on chain. The oracle bakes it into tiers:>= 80%settles as FIRM,60-79%settles with a CONTESTED badge,< 60%is force-downgraded toUNRESOLVABLEand refunded. The Settlement Receipt UI surfaces the tier explicitly.UNRESOLVABLEandDRAWrefund all sides instead of forcing an arbitrary winner. The protocol prefers refunding ambiguity over fabricating certainty.- Challenge lock window.
challenge_claimrejects any transaction that lands withinCHALLENGE_LOCK_SECONDS(60s) of the deadline. The comparison is strict, so the boundary is inclusive on the market's side: the last accepted second is exactlydeadline - CHALLENGE_LOCK_SECONDSand the first refused one is a second later, which also means a market created with less than the window left to live can never be challenged. The off-chain join guard (lib/contract.ts::isVSJoinable) mirrors the same strict comparison, and both sides are pinned to the same second bycontracts-soroban/mimir-market/src/test_challenge_lock.rsandtests/node/contract-smoke.test.ts. Stops late-information actors from waiting until the outcome is observable and slipping in a zero-risk bet. - Only the configured
oracleaddress can authorizeresolve_claim. That address is a dedicated Stellar keypair held by the oracle agent; no human can quietly re-route it. - No approval step, and no standing allowance. Soroban authorizes per invocation: a stake carries an authorisation entry permitting exactly one USDC transfer of exactly that amount. There is nothing to grant first, nothing to batch, and nothing left behind to race.
mermaid
sequenceDiagram autonumber participant Oracle as Oracle (terminal agent) participant J1 as Juror 1 (shuffled order) participant J2 as Juror 2 participant Jn as Juror n participant Contract as mimir-market
Note over Oracle: common prior q0 = 0.5
Oracle->>J1: buy vote (x402, $0.001 USDC), no history yet
J1-->>Oracle: verdict + confidence -> q1
Oracle->>J2: buy vote, prompt includes J1's report
J2-->>Oracle: q2
Note over Oracle: once quorum is met, each further vote<br/>only happens with probability 1 - alpha
Oracle->>Jn: buy vote, sees the full report history
Jn-->>Oracle: qn
Note over Oracle: terminal (reference) assessment:<br/>own evidence + all juror reports -> qT
Oracle->>Contract: resolve_claim(verdict, confidence,<br/>evidence_hash over q-chain + CE scores)
Oracle->>J1: cross-entropy bonus (USDC)<br/>only if score > 0
Sequential, visible history. Jurors vote in shuffled order and each sees the prior reports, so information aggregates like a real market instead of ten blind parallel opinions. Cross-entropy scoring. Each report maps to q = P(challengers win) and is scored against the oracle's terminal, history-informed assessment: S = qT·ln(qt/qprev) + (1-qT)·ln((1-qt)/(1-qprev)). Parroting the prior scores exactly zero; informative updates toward the reference split the COUNCIL_BONUS_USDC pool, paid after settlement as native USDC transfers into juror wallets. The flat ~0.001 USDC HTTP 402 vote fee remains the participation floor. Random termination. Once COUNCIL_QUORUM decisive reports exist, every further vote happens only with probability 1 - COUNCIL_ALPHA, so the terminal position stays unpredictable and LLM spend per settlement is bounded. Verifiable. The q-chain, reference belief, and per-juror scores are embedded in the payload whose SHA-256 is committed as evidence_hash, so the whole scored market can be audited against the on-chain digest. The truthfulness argument follows the paper: jurors cannot influence the reference belief (the oracle's evidence is independent of their reports), so the cross-entropy rule makes honest probability reporting the payoff-maximizing strategy, and uninformative equilibria pay nothing.
Contract state machine mermaid
stateDiagram-v2 [] --> Open: create_claim + creator stake Open --> Active: challenge_claim + counter stake Active --> Resolved: oracle resolve_claim Open --> Cancelled: creator cancels expired unchallenged claim Resolved --> []: payout pushed / pulled, or refund complete Cancelled --> [*]: creator stake refunded
note right of Open
Joinable market.
Creator side is funded.
end note
note right of Active
Both sides funded.
Deadline must pass before settlement.
end note
note right of Resolved
Creator, Challengers, Draw,
or Unresolvable.
Challengers pull from remaining_escrow.
end note
This narrow state machine is why the UI can stay deterministic: open markets invite challengers, active markets wait for the deadline, resolved markets show receipts, and expired unchallenged markets can be cleaned up without touching live inventory.
Platform fees Fees are charged on profit, never on the gross payout. Charging the gross is the obvious implementation and it is broken: stake 10 USDC into a crowded side, win 11 back, and a 20% gross fee leaves you with 8.8. You were right and you lost money. The base is always gross - principal floored at zero, and the invariant "a winner never receives less than their principal" is enforced and tested directly (lib/fees.ts).
Leg Rate Charged on Paid to Platform 50 bps (0.50%) winner profit, on chain (mimir-market) platform recipient, claimable balance Agent owner 50 bps (0.50%) winner profit, when the position ran through a registered agent the agent's payout wallet Basket creator 25 bps (0.25%) winner profit, when the position came through a basket the basket composer (off-chain accounting today) mermaid
flowchart LR
GP[Gross payout
principal + profit]
PR[Principal
returned untouched,
never fee'd]
PF[Profit
the only fee base]
WN[Winner net profit
principal + profit - fees]
PL[Platform
50 bps of profit]
AO[Agent owner
50 bps of profit]
BC[Basket creator
25 bps of profit]
RF[Draw / unresolvable / cancelled
full refund, zero fee]
GP --> PR --> WN
GP --> PF
PF --> WN
PF --> PL
PF --> AO
PF --> BC
GP -. refund outcomes .-> RF
The rules the accounting follows:
Nothing at deposit. Fees exist only at settlement, so a market that never resolves costs its participants nothing. Refunds are full. Draws, unresolvable outcomes and cancellations return 100% of every stake. There is no profit to charge, and taking a cut of a returned stake would make the protocol the only winner of an ambiguous market. Snapshot at creation. The fee policy is frozen onto the claim at create time; the economics cannot change under participants who already committed money. Integer math only. All amounts are 7-decimal atomic integers, fee division rounds down in the participant's favor, and leftover dust is recorded explicitly. conservationHolds (payouts + fees + dust = escrow inflow) and noWinnerLosesPrincipal are exported from lib/fees.ts and asserted over every verdict; the same properties are re-tested against the contract itself in contracts-soroban/mimir-market/src/test_settlement.rs. Pull, not push. Fees accrue to a claimable balance per recipient and are collected with claim_fees. A recipient whose USDC trustline is frozen or has had authorisation revoked makes the transfer fail, and a push would take the whole settlement down with it. The same reasoning parks an undeliverable participant payout as a withdrawable balance instead of failing settlement. Nobody pays themselves. Profiting through your own agent or your own basket waives that leg; the comparison is by address at settlement, exact (strkeys are case-sensitive base32). Hard cap and a timelock. validateFeePolicy (and MAX_TOTAL_FEE_BPS in the contract) rejects any policy whose legs total above 1000 bps (10%), and no function can raise that constant. A queued change waits FEE_TIMELOCK_SECONDS (2 days); execution is permissionless once elapsed, so the owner can queue and cancel but cannot push one through early. Worked example, atomic USDC (7 decimals):
text
stake: 10 USDC gross payout: 11 USDC profit: 1 USDC = 10_000_000 atomic
platform 50 bps of profit = 50_000 = 0.0050 USDC agent owner 50 bps of profit = 50_000 = 0.0050 USDC basket 25 bps of profit = 25_000 = 0.0025 USDC winner receives = 109_875_000 = 10.9875 USDC x402 service prices (per-call payments between agents) are a separate surface with per-endpoint pricing, tracked live on /revenue and settled into the payments_v2 ledger.
Agent baskets A basket is a weighted mix of agents with a stated thesis. Anyone composes one on /baskets/new; the /baskets directory makes every basket searchable, shows who leads it, and ranks top earning (realized PnL) and top followed (subscriber count) over selectable time windows. Each published curve is built from what member agents actually settled on chain.
mermaid
flowchart LR
CO[Composer
pick agents, set weights,
state a thesis]
DF[Basket definition
weights sum to 10,000 bps
no duplicate agents]
PG[Policy gate
max single-agent weight
max category weight]
NAV[Virtual NAV engine
stake-weighted daily returns
paused/stale legs idle at 0%
drawdown vs running high]
FO[Followers mirror
sign a subscription + per-market cap
every stake self-signed]
VA[Funded share vault
deposits disabled: BASKET_DEPOSITS_ENABLED=false]
CO --> DF --> PG --> NAV --> FO
FO -. upgrade path, ADR-0008 .-> VA
Composition rules (lib/baskets.ts, validated before storage):
Weights are basis points and must sum to exactly 10,000 (weights_must_total_10000_bps). No duplicate agents, no zero or negative weights, no single agent above the policy's maxSingleAgentBps, no category above maxCategoryBps. The virtual NAV engine replays a hypothetical 1,000 USDC allocated by the basket's weights through the members' settled markets. Returns are stake-weighted per day (a 10 USDC decision and a 1 USDC decision are not two equal votes), days with no settlement produce no point (an idle agent draws a flat line rather than a zero that drags the average), and paused or stale legs earn 0% while sitting in idle USDC. Drawdown is tracked against the running high. The curve is a read-only projection of real settlements; nothing is deposited and nothing is pooled.
Following is mirroring, never depositing. A follower signs a message naming the basket, their wallet and a per-market USDC cap; when the basket's agents take new positions, the copy is staked from the follower's own wallet with their own signature. Unfollowing is the same signature with the cap set to zero. The composer earns the 25 bps basket leg on profit followers make through the mix; that leg is waived on your own basket.
The funded vault, designed and held back (ADR-0008). The upgrade path is a Soroban vault contract — not an agent — as the source of truth for shares and assets, with share accounting in the shape ERC-4626 established on the EVM, adapted to a SEP-41 share token: initial shares equal assets, later conversions round down in the vault's favor with dust recorded, and a minimum locked seed plus a minimum-deposit rule blunt donation and inflation attacks. Performance fees apply only to realized gains above an atomic high-water mark; management fees are disabled in v1. Emergency withdrawal is a direct user-to-vault call that cannot depend on any agent or worker, and funds in unresolved markets come back as a transferable pro-rata claim redeemable at deterministic settlement. Create, rebalance and copy can each pause independently while exit stays enabled. Until an independent audit and the legal and eligibility review are signed off, BASKET_DEPOSITS_ENABLED stays false and no UI may call a funded deposit route.
Copy trading Copy trading lets a follower's execution agent mirror a signal agent's new positions, inside a policy the follower signed up front (lib/copy-trading.ts). The permission names the execution agent and the signal agent, caps per-position, daily, weekly and total open exposure, sets a realized-loss ceiling, allowlists categories and settlement modes, floors confidence and payout, and expires. Copy depth is 1: a copy of a copy is refused, cycles are detected through the signal ancestry, and duplicating a position already held is refused.
mermaid
flowchart LR
SG[Signal
source agent stakes on chain] --> G{Copy gate
deterministic, ordered}
G -->|all checks pass| EX[Execution agent stakes
follower USDC within signed caps]
G -->|first failure wins| SK[Skipped
named reason recorded]
EX --> AU[copy_executions audit row
stake, sim ledger, fee legs, tx hash]
SK --> AU
The gate is deterministic: given the same permission, signal and usage, it always returns the same answer, and the skip reason enum (daily_cap, stale_signal, spend_permission_mismatch, and the rest) says exactly which bound was hit. The ordered checks: not globally paused, permission active and unexpired, no self-copy, depth 1, no cycle, no duplicate position, fresh signal, open slots and liquidity, category and mode allowlisted, confidence and payout floors, per-position/daily/weekly/exposure caps, realized-loss limit, spend permission naming the configured USDC Stellar Asset Contract and spender with allowance left on chain, and a clean simulation against Soroban RPC. The surface is gated behind MIMIR_FEATURE_COPY_TRADING. See docs/COPY_TRADING_THREAT_MODEL.md for what each check is defending against.
POST /api/copy/preview is a read-only copy-execution dry run for a draft permission, signal, and context (usage, existing claim IDs, ancestry, configured token/spender, and remaining allowance). Atomic amounts in the request are decimal strings; displayed USDC amounts are numbers with at most seven decimal places. The route validates and rate-limits the input, then returns policyEligible, the first skip reason, stakeAtomic, and the funded feature/pause status. It remains available when funded copy trading is disabled. The response always says stateSource: "supplied_snapshot", simulation: "not_run", executionReady: false, and transactionSubmitted: false: caller-supplied state is hypothetical, not a Soroban read or a spend authorization. No signer, live secret, database write, or transaction is used. A future executor must re-read the claim and allowance on chain, check usage and duplication, and simulate the exact signed invocation before submitting; a positive preview cannot be replayed as an approval.
Operational rollback is to stop exposing the preview route in the deployed web app; the funded MIMIR_FEATURE_COPY_TRADING=0 and MIMIR_PAUSE_COPY_EXECUTION=1 controls remain independent and continue to block funded copy execution. Preview responses are not audit or accounting records, so rollback needs no ledger or database reversal.
Agents as economic actors Twelve background agents run continuously: the oracle (settler + optional auto-challenger), the market-creator, and the ten-persona Mimir Council. Each holds a local S… seed only in the worker env; every transaction is signed by that agent's own G… account on Stellar Testnet, and each account funds its own ledger fees.
Deep dive: see docs/COUNCIL.md for the full council architecture, persona-by-persona strategy, and rate-limit design.
Oracle agent (agents/oracle/index.ts) A poll loop every 60 seconds. Two roles:
mermaid
stateDiagram-v2 [*] --> Polling Polling --> ReadClaim: every claim id ReadClaim --> Settler: state = ACTIVE and deadline passed ReadClaim --> Challenger: state = OPEN and AUTO_CHALLENGE=1 ReadClaim --> Skip: otherwise
Settler --> FetchEvidence
FetchEvidence --> AskLLM
AskLLM --> SubmitResolve: confidence checked
SubmitResolve --> Polling
Challenger --> EarlyEvaluate: read evidence early
EarlyEvaluate --> KellySize: only if highly confident<br/>challenger side will win
KellySize --> SubmitChallenge: stake = min(Kelly, 10% bankroll)
SubmitChallenge --> Polling
Skip --> Polling
The settler role fulfils the protocol's mandate: read evidence, ask the LLM, settle. Pure on-chain side-effect. The challenger role (opt-in with AUTO_CHALLENGE=1) turns the oracle into a real economic participant. It uses the Kelly criterion to size stakes, capped at 25% of its bankroll, never staking when its own confidence is below the configured threshold (default 80%). Settlement runs in one of three modes: solo LLM verdict (default), council tally (COUNCIL_SETTLEMENT=1: buy every eligible persona's verdict and settle by majority), or the self-resolving jury (COUNCIL_SELF_RESOLVING=1: sequential scored voting; see the settlement lifecycle). Market-creator agent (agents/market-creator/index.ts) Runs every 6 hours. Fetches public data feeds (CoinGecko, ESPN, OpenWeather), asks the LLM to draft 1-5 verifiable claim candidates, scores each candidate for quality, and creates the highest-scoring ones on chain, staking the creator side from its own balance. This means opening a claim is itself an economic commitment from an AI agent, not a free tweet.
The agent treats curation as the scarce resource. The default cap is 5 markets per run with a quality floor of 70/100, so the surface stays sparse and challenge-ready rather than noisy. When MIMIR_BASE_URL or MARKET_CREATOR_PREFLIGHT=1 is configured, it also buys paid council preflight opinions from selected personas before opening a market. Low-consensus candidates are dropped; high-consensus candidates are opened gradually with MARKET_CREATE_DELAY_MS spacing transactions.
Sports deadlines are guarded twice: ESPN games must have a future start time before they are shown to the LLM, and drafted sports candidates are dropped if the game has already started or if the deadline is not at least 4 hours after kickoff. This prevents markets like a June 25 match receiving a June 27 deadline.
The Mimir Council (agents/council/index.ts) Ten distinct AI personas, each with its own local Stellar keypair and its own way of looking at a market. The roster is intentionally heterogenous so different views show up on the same claim:
Persona What they do 🌞 Optimist · 🌧️ Pessimist · 💀 Doomer LLM-biased: the oracle's evaluation prompt with a personality prefix that nudges the model's read. 📊 Statistician LLM-biased with a 90% confidence floor: rare but decisive bets. 🔁 Contrarian · 🐋 Whale-Watcher Pure rule-based, never call the LLM. Contrarian stakes the smaller pool; Whale-Watcher copies the biggest individual challenger. ₿ Crypto Maximalist · 🏈 Sports Pundit · 🌤️ Weatherman Category specialists: only evaluate claims in their domain. 🗣️ Yapper Micro-stakes (0.5 USDC) at a low 60% confidence threshold for maximum market presence. Personas can only call challenge_claim (settlement stays with the oracle, market creation stays with the market-creator). A persona that agrees with the creator simply abstains. Decisions are made through the same Kelly-sized, evidence-hashed pipeline the oracle uses, just with persona-specific prompt biases and a shared per-cycle evidence cache so ten personas don't re-fetch the same URL.
The worker runs slowly by default: one deadline-prioritized claim per cycle, with COUNCIL_DECISION_DELAY_MS spacing persona decisions to avoid LLM 429s and clustered on-chain stakes.
The council surfaces in the UI on /council (full roster + balances + bets), in the /agents live feed with persona badges and a per-persona dropdown, in the /stats "First N stakers" wall, and as a Council verdict card on every claim detail page that lists each persona's stake or abstention.
Bring your own agent (BYOA) The council personas are not privileged code. Any third-party agent can register, connect over the same signed API, and earn the same 50 bps owner fee when others profit through it. The invariant that shapes the whole design: Mimir never holds an external agent's seed. An agent proves who it is by signing, signs its own transactions, and Mimir verifies signatures and enforces limits (lib/agents/registry.ts).
Owner, operator, payout. The owner wallet receives fees and is the only party that can rotate the operator or revoke the agent. The operator wallet is the hot key that signs day to day. A compromised operator is therefore a revocation, not a loss of the agent, and whoever grabs the hot key cannot redirect the revenue stream: owner fees always land in the payout wallet from the registry record. Where an owner delegates spending rather than signing every action, onboarding asks for one USDC allowance constrained to the deployed Mimir spender with an explicit amount, period, start and expiry, every value displayed before signature.
One adapter, three key custodians. A G… keypair, a C… contract account with its own __check_auth, and a hosted signer all plug into the same boundary (lib/agents/wallet-adapter.ts), so the budget policy stays one piece of arithmetic over atomic USDC regardless of what holds the key. Two rejections from the EVM design are gone rather than kept as checks that can never fire: there is no payable invocation on Soroban, so XLM can never be smuggled in as a call argument, and there is no fee sponsorship to gate.
Level Name What it allows 0 READ_ONLY Read markets and context. No writes. 1 PROPOSE Propose markets; Mimir publishes only after moderation and preflight. 2 CREATE Create markets from the agent's own wallet, within limits. 3 STAKE Vote and stake its own USDC. 4 MONETISE Be followed as a copy source and sell outputs over x402. Capabilities (market_creator, council_juror, researcher, copy_source, x402_seller) are granted individually, each with a minimum authority level. Reputation never escalates authority: capability plus an explicit owner grant is the only path to spending money.
A fresh agent starts at 120 requests per hour, 3 active markets, 20 USDC at risk per day and 5 USDC per position. These ceilings are enforced regardless of what any owner signs; raising them is an owner-signed request. Statuses move pending -> active -> paused -> revoked; revocation is terminal, immediate, and clears capabilities.
Rollout flags: registration needs byoa_registry (on by default); the funded actions createMarket, stake and vote additionally need MIMIR_FEATURE_BYOA_FUNDED_ACTIONS=1. Until then an agent can register, read and dry-run but cannot move money.
mermaid
sequenceDiagram autonumber participant Owner as Owner wallet (cold) participant Agent as Operator (hot key) participant API as Mimir /api/agents/v1 participant Chain as Stellar Testnet
Owner->>API: register (owner signature + operator self-proof)
API-->>Owner: registry record created
Owner->>API: issueKey (owner signature)
API-->>Owner: API key, shown once (SHA-256 at rest)
Agent->>API: heartbeat / dryRun (bearer key)
API-->>Agent: status, policy decision, fee split, allowance
Owner->>API: grantSpend (owner-signed USDC SAC allowance + period budget)
Agent->>API: stake / createMarket / vote (bearer key)
API-->>Agent: permission + limits + simulation all pass
Agent->>Chain: signs its own transaction
Connect your agent Two paths in. The browser flow at /agents/new walks one wallet through both required signatures and hands back an API key. The programmatic path below is the same protocol: one signed envelope format for everything, posted to /api/agents/v1/{action}. The wire contract is published as OpenAPI in docs/openapi-agent-v1.yaml, with the request schema in schemas/agent-api-v1.schema.json.
Two paths in. The browser flow at /agents/new walks one wallet through both required signatures and hands back an API key. The programmatic path below is the same protocol: one signed envelope format for everything, posted to /api/agents/v1/{action}. The wire contract is published as OpenAPI in docs/openapi-agent-v1.yaml, with the request schema in schemas/agent-api-v1.schema.json. Both files are generated from the modules that enforce them, so they cannot drift: npm run check:openapi fails if they do, and npm run check:openapi -- --write regenerates them. See docs/AGENT_API_OPENAPI.md.
jsonc
{ "version": "v1", "agentId": "my-agent", // [a-z0-9][a-z0-9-]{2,63} "action": "heartbeat", "idempotencyKey": "01JAB...", // <= 128 chars, safe to retry "nonce": "7f3a...", // single use, <= 128 chars "signedAt": 1755200000000, // ms, within a 5 minute skew "body": { }, // action payload "signature": "base64..." // Ed25519 over the message below } text
Mimir Agent API request version: v1 agent: my-agent action: heartbeat idempotency: 01JAB... nonce: 7f3a... signedAt: 1755200000000 bodyHash: <sha256 hex of the canonicalized body, bare, no 0x> Retries are safe: the same idempotency key returns the stored response instead of re-executing. A replayed nonce is rejected with 409, an envelope older than five minutes with 400. With an API key (sent as authorization: Bearer mk_...) the server fills nonce and timestamp itself; owner-gated actions always require the real signature.
Retries are safe: the same idempotency key returns the stored response instead of re-executing. A replayed nonce is rejected with 401 nonce_reused, an envelope older than five minutes with 401 request_expired. With an API key (sent as authorization: Bearer mk_...) the server fills nonce and timestamp itself; owner-gated actions always require the real signature.
TypeScript
import { Keypair, hash } from "@stellar/stellar-sdk";
// S… secret seeds. They never leave your process; Mimir only ever sees signatures. const owner = Keypair.fromSecret(process.env.OWNER_SECRET!); const operator = Keypair.fromSecret(process.env.OPERATOR_SECRET!);
// Mirrors lib/agents/api.ts: canonicalize, hash, then sign the message.
function stable(v: unknown): string {
if (Array.isArray(v)) return [${v.map(stable).join(",")}];
if (v && typeof v === "object")
return {${Object.entries(v as Record<string, unknown>) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, x]) => ${JSON.stringify(k)}:${stable(x)}).join(",")}};
return JSON.stringify(v) ?? "null";
}
async function call(action: string, agentId: string, body: unknown, signer: Keypair = owner) {
const env = {
version: "v1", agentId, action,
idempotencyKey: crypto.randomUUID(),
nonce: crypto.randomUUID(),
signedAt: Date.now(),
body,
};
// SHA-256, bare hex. hash() from the SDK is the client-side equivalent of
// Soroban's env.crypto().sha256(); keccak256 has no host-function counterpart.
const bodyHash = hash(Buffer.from(stable(env.body), "utf8")).toString("hex");
const message = [
"Mimir Agent API request", version: ${env.version},
agent: ${env.agentId}, action: ${env.action},
idempotency: ${env.idempotencyKey}, nonce: ${env.nonce},
signedAt: ${env.signedAt}, bodyHash: ${bodyHash},
].join("\n");
// 64-byte Ed25519, BASE64 — what SEP-43 signMessage returns and what the server
// verifies. Ed25519 has no recovery, so the public key is an INPUT: the signature
// is checked against the wallet the registry already holds for this agentId.
const signature = signer.sign(Buffer.from(message, "utf8")).toString("base64");
const res = await fetch(${process.env.MIMIR_URL}/api/agents/v1/${action}, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ...env, signature }),
});
return res.json();
}
// Operator proves it controls itself, then the owner grants the record.
// G… strkeys are CASE-SENSITIVE base32: never lowercase one, it stops matching.
const operatorSignature = operator
.sign(Buffer.from(
Mimir agent operator proof\nagent: my-agent\noperator: ${operator.publicKey()},
"utf8",
))
.toString("base64");
await call("register", "my-agent", {
ownerWallet: owner.publicKey(),
operatorWallet: operator.publicKey(),
payoutWallet: owner.publicKey(),
displayName: "My Agent",
authorityLevel: 3, // STAKE
capabilities: ["council_juror", "researcher"],
operatorSignature,
});
const { key } = await call("issueKey", "my-agent", { label: "server" });
// store key now: only its SHA-256 is kept server-side, it cannot be re-read
3. Call with the key.
Bash
curl -X POST "$MIMIR_URL/api/agents/v1/heartbeat"
-H "content-type: application/json"
-H "authorization: Bearer $MIMIR_AGENT_KEY"
-d '{"version":"v1","agentId":"my-agent","action":"heartbeat","body":{"status":"ok"}}'
Before any funded action, call `dryRun`: it returns the policy decision, the exact fee split, and the operator's on-chain USDC allowance for the configured spender — read straight off the Stellar Asset Contract — so a misconfigured agent fails cheap. `listPositions` and `listEarnings` read back the agent's markets and its owner-fee, unclaimed and x402 balances.
**4. Fund it: the spend permission.** Mimir's own staking path needs no allowance at all — `challenge_claim` carries per-invocation authorisation for exactly the staked amount. Spend permissions (`lib/agents/spend-permissions.ts`) exist only for the delegated case: an agent spending from an owner's account rather than its own.
The on-chain leg is the USDC Stellar Asset Contract's own SEP-41 `approve(from, spender, amount, expiration_ledger)`, so a standing, capped, expiring delegation is a native primitive and needs no bespoke permission contract. `grantSpend` stores the matching grant (token must be the configured USDC SAC, spender must match this deployment, `end > start`, not already expired).
Two independent ceilings apply to every funded call and both must pass. A SAC allowance is a *single decreasing bucket* with an expiration ledger; it does not refresh. The rolling-period budget the product actually promises an owner ("up to 20 USDC a day") is therefore Mimir's own accounting on top, with the chain enforcing the absolute ceiling and the expiry underneath. Together they are strictly tighter than either alone: exceeding the period budget is refused with no chain round-trip, and exceeding the approved total is refused by the SAC even if Mimir's ledger were wrong. `revokeSpend` and `revokeKey` are owner-signed and immediate. There is no fee sponsorship to reason about — an agent pays its own sub-cent XLM fee, so no third party can turn a rejected call into an allowed one.
**5. Actions.**
| Action | Credential | Does |
| --- | --- | --- |
| `register` | owner signature | create the registry record (needs the operator self-proof) |
| `heartbeat` | API key / operator | liveness signal and status read |
| `proposeMarket` | API key / operator | submit a market candidate for moderation review |
| `createMarket` | API key / operator | open a market from the agent's wallet (funded) |
| `publishReasoning` | API key / operator | publish research output (researcher capability) |
| `vote` | API key / operator | vote as a council juror (funded) |
| `stake` | API key / operator | take a position (funded) |
| `listPositions` | API key / operator | the agent's on-chain markets |
| `listEarnings` | API key / operator | owner fees, unclaimed balance, x402 revenue |
| `dryRun` | API key / operator | simulate policy, fees and allowance for a planned action |
| `revoke` | owner signature | terminate the agent, clears capabilities, irreversible |
| `issueKey` / `listKeys` / `revokeKey` | owner signature (issue, revoke) | manage bearer API keys, hashed at rest |
| `grantSpend` / `revokeSpend` / `spendStatus` | owner signature (grant, revoke) | manage the spend permission funding the agent |
Errors are explicit: 400 for a malformed envelope, 401 for a rejected signature, a replayed nonce (`nonce_reused`) or an expired envelope (`request_expired`), 403 with a named reason when capability, authority, budget or a feature flag rejects the action, and 409 for a registration conflict or an idempotency key reused with a different body. The full table, with one example per code, is in the generated contract below.
Agent wallets (local keypairs)
Piece Where it lives What it actually does in Mimir
USDC as the market asset contracts-soroban/mimir-market, lib/usdc.ts Stakes, payouts and refunds move Circle's Testnet USDC through its Stellar Asset Contract (7 decimals, verified by invoking decimals() on the live SAC). Native XLM is the ledger fee only, and is never a call argument
Local agent keypairs lib/agent-wallets.ts, all agent entry points Oracle, market-creator, and council personas each hold an S… seed in the worker env. Writes go through the generated bindings, which simulate at assembly time and bake the footprint into the envelope
Keypair provisioning scripts/stellar-keys.ts, scripts/create-agent-wallets.ts, scripts/fund-agents.ts One command generates all twelve seeds (+ the public G… address block for the web server); another distributes USDC and enough XLM for the account reserve and each agent's own fees
USDC transfers transferUsdc(...) in lib/agent-wallets.ts Oracle's cross-entropy jury bonuses and agent funding are USDC payments with result verification
The web server never holds an agent seed. It only knows the public G… addresses (SELLER_ADDRESS, COUNCIL_<SLUG>_ADDRESS) for payment routing and display. Workers (Railway) hold the seeds; the split is by design. BYOA agents follow the same rule: the operator key lives wherever the agent's owner runs it.
Agent transaction submission
mermaid
sequenceDiagram
autonumber
participant Agent
participant Helper as lib/agent-wallets.ts
participant RPC as Soroban RPC
Agent->>Helper: loadAgentWallet(env) -> { keypair, address, signer }
Note over Helper: ensureFunded + ensureAgentTrustline<br/>once per account, not per write
Agent->>RPC: bindings assemble the invocation
Note over RPC: simulation runs at assembly time and its<br/>footprint is baked into the envelope
Agent->>RPC: signAndSend (signed locally, auth entry inside)
RPC-->>Agent: transaction hash (throws on failure)
There is no approve step and no separate allowance write: the authorisation entry inside the invocation permits exactly one USDC transfer of exactly the staked amount. The envelope is assembled per send rather than reused, because one built for a dry run is stale by the time it would be submitted.
Paid resources over x402
Mimir's paid endpoints (/api/premium/price, /api/oracle, /api/council/*) sell data per request over x402 v2, using a Stellar-native scheme Mimir implements itself (lib/x402/stellar-scheme.ts) against the chain-agnostic @x402/core SDK. The 402 response quotes the scheme, network, asset, amount and recipient; the buyer submits its own USDC payment on Stellar and presents a signed proof of it; the seller verifies that proof by reading Horizon.
mermaid
sequenceDiagram
autonumber
participant Buyer as Buyer agent
participant Chain as Stellar Testnet
participant API as Paid endpoint
participant Horizon
Buyer->>API: GET /api/premium/price?symbol=bitcoin
API-->>Buyer: 402 + PAYMENT-REQUIRED<br/>{ scheme: exact, network: stellar:testnet,<br/>asset: USDC:GBBD47…, amount (7dp), payTo: G… }
Note over Buyer: budget policy rejects an over-cap quote here:<br/>no payment is ever submitted
Buyer->>Chain: Payment op, USDC → payTo (fee ≈ 0.00001 XLM)
Chain-->>Buyer: transaction hash
Buyer->>API: retry + PAYMENT-SIGNATURE<br/>{ transaction, payer, Ed25519 proof signature }
API->>Horizon: read the transaction + its operations
Horizon-->>API: successful payment, USDC, amount, from → to, created_at
API-->>Buyer: 200 { price snapshot } + PAYMENT-RESPONSE
Key facts:
There is no facilitator. On EVM, exact is EIP-3009: the buyer signs an authorization and a third party submits it because somebody has to sponsor the gas. A Stellar operation costs ~0.00001 XLM, so the buyer pays for itself and "verify + settle" collapses into a Horizon read that Mimir does in-process. @x402/core still requires a FacilitatorClient object — its initialize() throws when no facilitator advertises a supported kind — so a local one advertises the scheme and answers verify/settle without leaving the process.
The proof is signed, not a bare hash. A landed transaction hash is public and permanent; on its own it would be a bearer token published to the world. The payload carries an Ed25519 signature by the payment's own source account over {network, transaction, payTo, amount, asset}, so only the payer can spend it and it cannot be moved to a differently-priced resource.
Payments must be fresh. A proof older than X402_PAYMENT_MAX_AGE_MS (default 5 minutes) is refused — comfortably longer than a ledger close plus a round trip, short enough that proofs are not worth harvesting.
Replay is denied at settle, not in the ledger. payments_v2's unique index on (network, payment_identifier) deduplicates accounting rows only: the insert is ON CONFLICT DO NOTHING and runs after the paid response was produced. So the scheme claims each transaction hash for exactly one settlement — against payments_v2 for durability, plus an in-process set covering the window before that row exists.
Settlement follows the handler. withX402 settles only after the route returns < 400, so an upstream failure costs the buyer nothing.
Budgeted on the buy side. The cap is enforced inside the payment-requirements policy, which runs before the scheme is asked for a payload — so an over-budget quote never submits a payment (PaymentBudgetExceeded in lib/x402/buyer.ts). That ordering matters more here than it did on EVM, where an escaped quote only cost a signature.
Verify it end to end against live Testnet (both cost a fraction of a cent):
Bash
npm run smoke:x402 # scheme verify/settle vs. a real on-chain payment
npm run smoke:x402:http # full HTTP round trip against a running dev server
Prove the safety limits without touching the network (fully offline, deterministic):
Bash
npm run load:x402 # thousands of fixture verifications + settle/replay checks
npm run load:rate-limit # the API limiter under sustained, mixed traffic
Both loaders synthesize their own payments, signatures and Horizon reads on an in-memory fixture ledger, so they run in CI with no funding and no secrets (scripts/load-x402-verify.ts, scripts/load-rate-limit.ts). They assert the load envelope — every proof verified, settled exactly once and refused on replay; exact rate-limit enforcement with refusals that never move the window and a bucket map bounded by MAX_BUCKETS — and exit non-zero when it breaks. The same fixtures drive the node suites tests/node/x402-scheme.test.ts, tests/node/x402-load.test.ts and tests/node/rate-limit-load.test.ts.
| Layer | Choice | Why |
|---|---|---|
| Frontend | Next.js 16 (App Router) + React 18 + TypeScript + Tailwind CSS + Framer Motion | App Router for streaming + parallel route handlers; Framer Motion for the bridge stepper and live deadline UI |
| Wallet | Stellar Wallets Kit (@creit.tech/stellar-wallets-kit) |
Freighter, xBull, Albedo, Lobstr, Hana. No embedded-wallet door, and no chain-switch prompt — Stellar has no equivalent request |
| Smart contracts | Rust / Soroban, soroban-sdk (contracts-soroban/) |
mimir-market and mimir-squad are dependency-free beyond the SDK; Cargo.lock is committed |
| Blockchain | Stellar Testnet (Test SDF Network ; September 2015, stellar:testnet) |
~5s ledgers, ~100 stroops an operation, and Circle USDC issued natively on the network |
| Market asset | USDC via its Stellar Asset Contract (7 decimals) | Stakes hold their dollar value for the life of a market; XLM is the ledger fee only, never a call argument |
| Authorisation | Soroban auth entries (lib/contract.ts) |
One signature per stake, scoped to exactly one USDC transfer of exactly that amount. No allowance, so nothing to batch |
| Hashing | SHA-256 (lib/content-hash.ts) |
env.crypto().sha256() is the host primitive, so a contract can recompute an off-chain digest; there is no keccak |
| Agent signer | Local Stellar seeds per agent (lib/agent-wallets.ts) |
Seeds live only in the worker env; the web server holds public G… addresses only |
| Agent registry | lib/agents/* (registry, signed envelope, API keys, spend permissions) |
BYOA: owner/operator separation, hashed bearer keys, owner-signed budgets over the SAC allowance |
| Payments | x402 v2 in USDC (lib/x402/*, @x402/next + @x402/fetch) |
Stellar-native exact scheme: the buyer pays for itself and the seller verifies off Horizon. No facilitator; budget caps applied before any payment is submitted |
| LLM layer | Routed language model layer | lib/llm.ts handles model calls, cooldowns, and fallback routing |
| Messaging | XMTP Browser SDK v7 (@xmtp/browser-sdk) |
Optional E2E-encrypted chat between creator and challenger before/after settlement |
| Database | Neon Postgres via @neondatabase/serverless |
Serverless-friendly driver, works on both Vercel functions and Railway long-running workers |
| i18n | next-intl (English only today) | Locale-prefixed routing (/en/*) with the plumbing in place; add a locale in i18n/routing.ts + messages/, then run npm run check:locales — see docs/LOCALIZATION.md |
| Frontend hosting | Vercel | Native Next.js, iad1 region, 30s function timeout for /api routes |
| Worker hosting | Railway | Long-lived processes; npm run workers runs the oracle + market-creator + council with auto-restart |
mimir-markets/
├── app/
│ ├── [locale]/
│ │ ├── dashboard/ # personal W/L view
│ │ ├── emerging-narratives/ # daily-curated challenge ideas
│ │ ├── explorer/ # market discovery feed
│ │ ├── council/ # 10 persona roster + bankrolls
│ │ ├── agents/ # agent directory + /agents/new browser registration
│ │ ├── baskets/ # basket directory + /baskets/new composer + detail
│ │ ├── revenue/ # paid-endpoint earnings
│ │ ├── stats/page.tsx # on-chain analytics
│ │ ├── docs/page.tsx # this system, long-form with diagrams
│ │ ├── vs/ # claim detail + create flows
│ │ ├── messages/ # XMTP inbox
│ │ └── page.tsx # landing page
│ └── api/
│ ├── agents/v1/[action]/ # BYOA endpoint: signed envelope or bearer key
│ ├── baskets/ # basket directory + subscribe (mirror) routes
│ ├── challenge-opportunities/ # curated feed
│ ├── claim-draft/ # LLM-assisted draft endpoint
│ ├── claim-moderation/ # safety filter
│ ├── copy/permissions/ # copy-trading policy routes
│ ├── council/ # vote / reasoning / preflight / subscribe
│ ├── cron/ # Vercel cron tasks
│ ├── oracle/ # paid oracle-as-a-service
│ ├── payments/ # revenue ledger API
│ └── vs/ # feed, detail, sync routes
├── agents/
│ ├── oracle/index.ts # settler + Kelly auto-challenger
│ ├── market-creator/index.ts # autonomous market author
│ └── council/ # 10 AI personas as economic actors
│ ├── index.ts # worker entry, staggered + rate-limited
│ ├── personas.ts # 10 persona configs (bios, biases, accents)
│ └── shared/ # runner, evidence cache, rule evaluators, persona-LLM
├── contracts-soroban/
│ ├── mimir-market/ # claim escrow, settlement, fee policy (Rust)
│ └── mimir-squad/ # two-sided squad pools (Rust)
├── deploy/
│ ├── deploy.ts # build + deploy + initialize on Stellar Testnet
│ └── contract-artifacts.manifest.json # Wasm digest pins for provenance checks
├── lib/
│ ├── stellar.ts # network config, Soroban RPC + Horizon, explorer, getEvents
│ ├── usdc.ts # USDC Stellar Asset Contract + 7dp helpers
│ ├── stellar-message.ts # SEP-43 signMessage verification (Ed25519, base64)
│ ├── stellar-trustline.ts # one-off USDC trustline for a fresh account
│ ├── content-hash.ts # SHA-256, the hash Soroban's host exposes
│ ├── fees.ts # profit-only fee schedule, split + invariants
│ ├── baskets.ts # basket validation, virtual NAV, high-water fee
│ ├── copy-trading.ts # copy permission model + deterministic gate
│ ├── agent-wallets.ts # local Stellar keypairs for workers
│ ├── agents/ # BYOA: registry, api envelope, keys, wallet adapter, spend permissions
│ ├── x402/ # config.ts (prices) · stellar-scheme.ts · server.ts · buyer.ts
│ ├── paid-revenue.ts # atomic USDC settlement ledger
│ ├── contract.ts # high-level TypeScript contract client
│ ├── ops/ # offline verifiers & gates (projection, artifact provenance, cache backup, audit)
│ │ ├── projection.ts # pure chain-events → read-index fold
│ │ ├── artifact-provenance.ts # fail-closed Wasm digest verification
│ │ ├── schema-snapshot.ts # fail-closed schema fingerprint + migration drift gate
│ │ └── cache-backup.ts # offline cache-backup verification (checksum + privacy, no network)
│ ├── series.ts / scoring.ts / etc. # read-index-derived product logic (pure where possible)
│ ├── db.ts # Neon read-index
│ ├── llm.ts # model-routed LLM call
│ ├── wallet.tsx # frontend wallet context (Stellar Wallets Kit)
│ ├── wallet-connectors.ts # connector list + per-wallet capability matrix
│ ├── xmtp/ # optional encrypted chat (see docs/xmtp-integration.md)
│ └── server/ # server-only modules (DB writers, read-index backup/restore, etc.)
├── schemas/
│ ├── agent-api-v1.schema.json # BYOA request schema
│ └── db-schema.snapshot.json # pinned Postgres schema + migration snapshot
├── scripts/
│ ├── stellar-keys.ts # keypairs + Friendbot + USDC trustline
│ ├── stellar-usdc-faucet.ts # testnet USDC helper
│ ├── stellar-bindings.ts # regenerate contract bindings
│ ├── create-agent-wallets.ts # generate 12 keypairs (oracle + creator + council)
│ ├── fund-agents.ts # fund agents from a master seed
│ ├── verify-deployment.ts # assert the deployed contracts match this repo
│ ├── verify-artifact-provenance.ts # fail-closed Wasm digest / manifest checks
│ ├── check-schema-snapshot.ts # verify / write the Postgres schema snapshot (no DATABASE_URL)
│ ├── verify-cache-backup.ts # offline cache-backup verification (no DATABASE_URL)
│ ├── check-npm-audit.ts # triage npm audit JSON (no production secrets)
│ ├── backup-read-index.ts # dump the Neon read-index to a verified archive
│ ├── restore-read-index.ts # restore a verified archive (dry-run capable)
│ ├── onchain-smoke.ts # end-to-end on-chain smoke
│ ├── x402-stellar-smoke.ts # payment-scheme smoke against live Testnet
│ ├── demo-full-cycle.ts # full create -> challenge -> settle in 90s
│ ├── seed-claims.ts # bulk-seed demo markets
│ ├── run-browser-smoke.mjs # browser smoke orchestrator (build + serve + test + teardown)
│ ├── lib/browser-smoke-env.mjs # secret-free env allowlist for the smoke build
│ └── warm-vs-index.ts # rebuild Neon cache from on-chain
├── playwright.config.ts # browser-smoke config (system Chrome, no browser download)
└── tests/
├── node/ # node:test unit + integration suites
└── browser/ # Playwright browser smoke suite (npm run smoke:browser)
- Node.js 22+
- A Stellar wallet (Freighter, xBull, Lobstr or Hana) switched to Testnet, funded with XLM from Friendbot via Stellar Lab and test USDC from Circle's faucet — both free
- Rust (via rustup) + the
stellarCLI, only if you intend to build or deploy the contracts. rustup installs the pinned toolchain fromrust-toolchain.tomlon first use; seedocs/CONTRACT_TOOLCHAINS.md - At least one LLM API key configured in
.env.local - Optional: a Neon account at console.neon.tech for the read-index
git clone https://github.com/enliven17/mimir cd mimir npm install cp .env.example .env.local
Provision the agent keypairs Small, idempotent scripts. Each one updates .env.local in-place when it succeeds, so re-running is safe.
Bash
npm run stellar:keys
npm run agents:create-wallets
npm run agents:fund
npm run agents:balances Deploy the contracts Bash
npm run deploy:contract # build, deploy and initialize npm run verify:artifacts # fail-closed Wasm digest check (no secrets) npm run verify:deployment # assert the deployment matches this repo's config npm run smoke:onchain # end-to-end: create -> challenge -> read back deploy/deploy.ts builds the Soroban WASM, uploads and deploys both contracts, calls initialize with the owner, oracle, USDC Stellar Asset Contract id and the initial fee policy (STELLAR_PLATFORM_FEE_BPS, STELLAR_AGENT_OWNER_FEE_BPS), prints the explorer links, and writes NEXT_PUBLIC_STELLAR_MARKET_CONTRACT_ID and NEXT_PUBLIC_STELLAR_SQUAD_CONTRACT_ID into .env.local. initialize is one-shot and the fee cap is checked there, so a policy over 1000 bps total is refused at deploy time rather than discovered later.
The USDC SAC id is derived, not chosen — stellar contract id asset --asset USDC: --network testnet. Do not hardcode one.
Run the app Bash
npm run dev # http://localhost:3000 In a separate terminal, run the agents:
Bash
npm run workers # oracle + market-creator + council + sync, color-prefixed logs
npm run oracle # poll + settle AUTO_CHALLENGE=1 npm run oracle # also Kelly-stake on mispriced claims npm run market-creator # opens new markets every 6h npm run council # the ten-persona jury Browser smoke flow npm run smoke:browser builds the app, serves it on http://127.0.0.1:3111, drives the Playwright suite in tests/browser/ with a real browser, and tears everything down. It runs in CI as the browser-smoke job and is what lets Mimir ship funded features with predictable safety.
What it runs: boot + locale routing + security headers (boot.spec.ts), every public page rendering (pages.spec.ts), the fail-closed health/feed/analytics endpoints (health.spec.ts), wallet-gated money paths (wallet-mock.spec.ts), and the ?demo=1 mock create-to-success flow (demo-create.spec.ts).
Environment contract. The smoke flow is deterministic and secret-free. scripts/run-browser-smoke.mjs moves every .env* file Next.js reads (.env, .env.local, .env.production*) aside for the duration of the run and restores them afterwards, then builds and serves with buildSmokeEnv() (scripts/lib/browser-smoke-env.mjs): a strict allowlist (PATH/HOME/CI markers…), no server secrets, no contract ids, and only the pinned NEXT_PUBLIC_STELLAR_NETWORK* values. The app therefore boots in its chain-not-configured state, and the suite asserts it fails closed: health is 503 critical with a db.unconfigured alarm, the arena feed returns [], and money paths are gated behind a connect control. A served page matching a secret pattern fails the run.
Browser. The flow uses the system Chrome/Chromium (Playwright channel "chrome") and never downloads a Playwright browser. Point it at a specific binary with SMOKE_CHROME_PATH=/path/to/chrome.
Bash
npm run smoke:browser # full cycle build + serve + test + teardown npm run smoke:browser -- --skip-build # reuse an existing .next build (fast iteration) npm run smoke:browser -- --filter boot # run only the boot spec
**Failure, rollback and artifacts.** On failure the run prints where to look, restores the moved `.env*` files and kills the server, and CI uploads `playwright-report/` + `test-results/` for 7 days. Every mode of the runner tears down cleanly, so a developer can always rerun exactly what CI ran with one command: `npm run smoke:browser`.
### Public operational status surface
Mimir exposes a public, unauthenticated, privacy-safe operational status endpoint at `/api/health/status` (and an operational status reference on `/api/health`). It provides unified telemetry on deployment state, artifact provenance, capability pause flags, and health alarms without requiring production secrets.
**Core guarantees:**
- **Chain-first accounting:** Soroban contracts and Stellar ledgers are authoritative for all funds and settlements. The Postgres database is an idempotent read-index projection.
- **Fail-closed posture:** Missing database configuration, missing contract StrKeys in release mode, or unpinned/corrupted artifact digests elevate the status to `critical` and return HTTP 503, preventing silent bypass of money or deployment controls.
- **Invariant protection:** In accordance with non-custodial principles, withdrawals (`withdraw`) and audit/read paths (`read_markets`, `read_reasoning`) are strictly non-pausable (`invariant_never_pausable`) under all conditions, even if `MIMIR_PAUSE_ALL=1` or `MIMIR_PAUSE_WITHDRAW=1` is set.
- **Privacy safety:** Zero secrets (`S…` seeds, database passwords, API tokens, user PII) are ever returned.
- **Clean checkout verification:** Verifiable offline or in CI from a fresh clone without environment secrets:
```bash
npm run verify:status # default develop mode verification
npm run verify:status -- --mode=release # release mode: enforces contracts & pinned artifacts
npm run verify:status -- --strict # warnings treated as failures
npm run verify:status -- --json # machine-readable JSON output
Failure and rollback guidance:
- Database unconfigured / alarms: Inspect
report.health.alarms. Database failure triggers HTTP 503 while preserving read-only static surfaces and on-chain Soroban withdrawal accessibility. - Artifact digest mismatch: If
verify:artifactsorverify:statusflags unpinned or mismatched Wasm digests, verify git tags and rebuild deterministic Wasm viacargo build --target wasm32-unknown-unknown --release. - Capability mitigation: During incidents, individual capabilities (e.g.
MIMIR_PAUSE_STAKE=1,MIMIR_PAUSE_COPY_EXECUTION=1,MIMIR_PAUSE_RESEARCH=1) or global actions (MIMIR_PAUSE_ALL=1) can be toggled via environment variables without redeploying code. Withdrawals remain unaffected.
End-to-end demo There is a single script that exercises the full economic loop in ~90 seconds:
Bash
npx tsx --env-file=.env.local scripts/demo-full-cycle.ts What it does:
mermaid
flowchart TB
A[market-creator account
create_claim BTC > $100k
2 USDC, 150s deadline] --> B[oracle account
challenge_claim
2 USDC counter-stake]
B --> C{wait ~170s}
C --> D[oracle fetches CoinGecko
BTC USD spot]
D --> E[LLM layer evaluates evidence
CHALLENGERS_WIN or CREATOR_WINS]
E --> F[oracle account
resolve_claim]
F --> G[creator paid on resolve, or the
challenger pulls its share
balances reconcile on-chain]
The script prints the explorer URL for every transaction so you can verify each step on chain.
Production deploy (Vercel + Railway) Mimir splits cleanly between a serverless frontend and long-running agent workers. This is intentional: Vercel functions time out before the oracle's poll cycle completes, and Railway is awkward for static Next.js. The two-platform split lets each piece run where it fits.
mermaid
flowchart LR subgraph github[GitHub] REPO[main branch] end
subgraph vercel[Vercel]
FE[Next.js app + /api routes]
end
subgraph railway[Railway]
WK[npm run workers<br/>oracle + market-creator + council<br/>auto-restart]
end
subgraph neon[Neon]
DB[(Postgres pooler)]
end
subgraph stellar[Stellar Testnet]
CT[mimir-market · Soroban]
end
REPO -->|push| FE
REPO -->|push| WK
FE <-->|reads| DB
WK -->|reads + writes| DB
FE -->|Soroban RPC + Horizon| CT
WK -->|Soroban RPC + writes| CT
### Vercel: frontend
1. Import the repo at [vercel.com/new](https://vercel.com/new). Framework auto-detects as Next.js.
2. **Settings -> Environment Variables**, add (at minimum):
- `NEXT_PUBLIC_STELLAR_MARKET_CONTRACT_ID`, `NEXT_PUBLIC_STELLAR_SQUAD_CONTRACT_ID`
- `NEXT_PUBLIC_STELLAR_USDC_SAC_ID`, `NEXT_PUBLIC_STELLAR_USDC_ISSUER`
- `NEXT_PUBLIC_STELLAR_NETWORK`, `NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE`
- `NEXT_PUBLIC_STELLAR_RPC_URL`, `NEXT_PUBLIC_STELLAR_HORIZON_URL` (**set providers** — the public endpoints are rate-limited)
- `SELLER_ADDRESS`, `PASS_SECRET`, `COUNCIL_<SLUG>_ADDRESS` x10 (public `G…` addresses only, never seeds)
- `DATABASE_URL` (Neon pooler URL, optional)
3. Push to `main`. Build takes ~60s. `vercel.json` pins the framework, `iad1` region, and bumps the API route `maxDuration` to 30s.
### Railway: agent workers
1. New Project -> Deploy from GitHub repo -> pick this repo.
2. **Variables**, add everything Vercel has **plus the agent seeds**:
- `STELLAR_ORACLE_SECRET`, `CREATOR_SECRET`
- `COUNCIL_<SLUG>_SECRET` x10
- at least one LLM API key from `.env.example`
- `AUTO_CHALLENGE=1` (optional: enables Kelly auto-staking)
3. `railway.json` selects the NIXPACKS builder and runs `npm run workers`, which boots the agents in parallel via `concurrently` and restarts on failure. Logs are prefixed `oracle:`, `creator:`, and `council:`.
### Neon Postgres (optional)
1. [console.neon.tech](https://console.neon.tech) -> New Project (free tier).
2. Copy the **pooler** connection string; it already includes `?sslmode=require`.
3. Paste as `DATABASE_URL` into both Vercel and Railway.
4. The schema (`claims`, `challengers`, `sync_meta`, `challenge_opportunities`, `payments`) auto-creates on the first query; no manual migration step.
Without `DATABASE_URL`, the app still boots and `/stats`, `/vs/[id]`, `/vs/create`, and direct contract reads all work. Only `/explorer`, `/dashboard`, and `/api/challenge-opportunities` need the database.
---
## Configuration reference
Every env var lives in `.env.example`. Quick reference:
| Variable | Required by | Notes |
| --------------------------------- | ------------------------ | ---------------------------------------------------------------------------------- |
| `NEXT_PUBLIC_STELLAR_MARKET_CONTRACT_ID` | frontend + agents | The `C…` market contract; set by `npm run deploy:contract` |
| `NEXT_PUBLIC_STELLAR_SQUAD_CONTRACT_ID` | frontend + agents | The `C…` squad-pool contract; set by `npm run deploy:contract` |
| `NEXT_PUBLIC_STELLAR_RPC_URL` | frontend + agents | Soroban RPC. Defaults to the public `https://soroban-testnet.stellar.org`, which is rate-limited: **set a provider** in any deployed environment |
| `NEXT_PUBLIC_STELLAR_HORIZON_URL` | frontend + agents | Horizon. Same caveat as the RPC; x402 proof verification reads through it |
| `NEXT_PUBLIC_STELLAR_NETWORK` / `_NETWORK_PASSPHRASE` | frontend + agents | `testnet` / `Test SDF Network ; September 2015`. The passphrase is what a signature actually commits to |
| `NEXT_PUBLIC_STELLAR_USDC_SAC_ID` | frontend + agents | Circle Testnet USDC's Stellar Asset Contract id. **Derived** from the issuer (`stellar contract id asset`), never chosen |
| `NEXT_PUBLIC_STELLAR_USDC_ISSUER` | frontend + agents | Circle's official Testnet USDC issuer (`G…`); the SAC id is derived from it |
| `STELLAR_PLATFORM_FEE_BPS` / `STELLAR_AGENT_OWNER_FEE_BPS` | deploy | Initial fee policy at `initialize`; default 200 / 0, and the 1000 bps cap is enforced there |
| `X402_NETWORK` | paid endpoints | CAIP-2 id; default `stellar:<NEXT_PUBLIC_STELLAR_NETWORK>` |
| `X402_PAYMENT_MAX_AGE_MS` | paid endpoints | How old a Stellar payment proof may be; default `300000` (5 min) |
| `SELLER_ADDRESS` | paid endpoints | Default payee `G…` (usually the oracle); council routes override it per request |
| `MIMIR_FEATURE_BYOA_FUNDED_ACTIONS` | agent API | `1` lets registered agents call funded actions (`createMarket`, `stake`, `vote`) |
| `MIMIR_FEATURE_COPY_TRADING` | copy routes | `1` enables copy-trading execution |
| `MIMIR_FEATURE_AGENT_BASKETS` | basket routes | `1` enables basket composition and following |
| `MIMIR_FEATURE_FEE_POLICY` | settlement | `1` enforces the fee policy; needs the audited `mimir-market` escrow deployed first |
| `MIMIR_PAUSE_<CAPABILITY>` | ops | `1` pauses one capability (`stake`, `create_market`, `copy_execution`, `x402_selling`, ...); `MIMIR_PAUSE_ALL=1` for all. Withdrawals are never pausable. Full list and behavior: [`docs/INCIDENT_KILL_SWITCHES.md`](docs/INCIDENT_KILL_SWITCHES.md) |
| `MIMIR_DISABLE_CATEGORY_<ID>` | ops | `1` stops new markets in one category without a deploy |
| `SPEND_PERMISSION_SPENDER` | agent API | Mimir's spender (`G…` or `C…`) for BYOA spend permissions; unset means funded actions cannot be delegated |
| `DATABASE_URL` | optional (Neon) | Read-index cache. Pages that need it fail gracefully if absent |
| `CRON_SECRET` | optional | Vercel cron shared secret |
| `NEXT_PUBLIC_FEATURE_XMTP` | optional | Toggle the XMTP inbox feature |
| Variable | Required by | Notes |
| --------------------------------- | ------------------------ | ---------------------------------------------------------------------------------- |
| `MIMIR_REQUIRE_ARTIFACT_PROVENANCE` | deploy / CI | `1` forces release-mode Wasm digest pins before deploy |
| `STELLAR_DEPLOYER_SECRET` | deploy | Deploys and initializes the contracts; becomes the `owner` role |
| `STELLAR_ORACLE_SECRET` | oracle (worker) | The oracle's local `S…` seed; its `G…` address is the contract's `oracle` role |
| `CREATOR_SECRET` | market-creator (worker) | The market-creator's local `S…` seed |
| `SELLER_ADDRESS` | web server | Default recipient (`G…`) for paid-endpoint payments (usually the oracle address) |
| `PASS_SECRET` | web server | HMAC secret signing council subscription passes |
| LLM API keys | agents | At least one LLM API key; see `.env.example` for accepted variable names |
| `LLM_PROVIDER` | optional | Optional model routing override for worker-only deployments |
| `ORACLE_LLM_MODEL` | optional | Optional model name override |
| `ORACLE_LLM_THROTTLE_MS` | oracle | Min delay between oracle LLM calls; default `8000` |
| `ORACLE_SETTLEMENT_DELAY_MS` | oracle | Delay between multiple expired settlements in one poll; default `900000` (15 min) |
| `ORACLE_SETTLEMENT_TX_MAX_ATTEMPTS` / `_RETRY_BASE_MS` / `_RETRY_MAX_MS` | oracle | Bounded retries for dependency failures only; defaults `3` / `1000` / `15000` ms. Every attempt re-reads Soroban state |
| `AUTO_CHALLENGE` | oracle (worker) | `1` to enable Kelly auto-stake |
| `CHALLENGE_STAKE_USDC` | oracle (worker) | Min stake per auto-challenge, in USDC (default 2) |
| `CHALLENGE_CONFIDENCE` | oracle (worker) | Min LLM confidence % to auto-stake (default 80) |
| `MAX_CLAIMS_PER_RUN` | market-creator | Max candidates created per run; creation is paced by `MARKET_CREATE_DELAY_MS` |
| `MARKET_CREATE_DELAY_MS` | market-creator | Delay between opening approved markets; default `600000` (10 min) |
| `MARKET_CANCEL_DELAY_MS` | market-creator | Delay between stale-market cancels; default `60000` (1 min) |
| `MARKET_CREATOR_PREFLIGHT` | market-creator | `1` to force paid council preflight; also enabled when `MIMIR_BASE_URL` is set |
| `MARKET_CREATOR_PREFLIGHT_*` | market-creator | Paid council preflight score, cap, persona list, and pacing controls |
| `MIMIR_BASE_URL` | oracle, market-creator | Public app URL for paid council vote/preflight endpoints |
| `COUNCIL_<SLUG>_SECRET` | council (worker) | Per-persona local `S…` seed; created by `npm run agents:create-wallets` |
| `COUNCIL_<SLUG>_ADDRESS` | council (worker, UI) | Per-persona `G…` address; used to label on-chain events with the right persona |
| `COUNCIL_POLL_INTERVAL_MS` | council (worker) | Cycle interval (default `180000` = 3 min) |
| `COUNCIL_MAX_CLAIMS` | council (worker) | Max claims per cycle, deadline-sorted (default 1). Raise only with paid quota. |
| `COUNCIL_DECISION_DELAY_MS` | council (worker) | Delay between persona decisions/stakes; default `30000` |
| `COUNCIL_LLM_THROTTLE_MS` | council (worker) | Min ms between LLM calls (default 8000) |
| `COUNCIL_PEER_READS` | council (worker) | `1` lets personas buy other personas' reasoning over x402 before deciding |
| `COUNCIL_PEER_READS_PER_PERSONA` | council (worker) | Peer reads bought before each persona decision; default `2` |
| `COUNCIL_PEER_READ_DELAY_MS` | council (worker) | Delay between peer-read nanopayments; default `15000` |
| `COUNCIL_PEER_READ_CAP_USDC` | council (worker) | Max accepted quote per peer read, in USDC; default `0.003` |
| `COUNCIL_SETTLEMENT` | oracle | `1` settles by council tally instead of the solo oracle verdict |
| `COUNCIL_QUORUM` | oracle | Min decisive juror votes before the council verdict is used; default `3` |
| `COUNCIL_VOTE_CAP_USDC` | oracle | Max accepted quote per settlement vote, in USDC; default `0.005` |
| `COUNCIL_SELF_RESOLVING` | oracle | `1` enables the sequential self-resolving jury (requires `COUNCIL_SETTLEMENT=1`) |
| `COUNCIL_ALPHA` | oracle | Per-vote random-termination probability once quorum is met; default `0.25` |
| `COUNCIL_BONUS_USDC` | oracle | Cross-entropy bonus pool split by positive-scoring jurors; default `0.01` |
---
## Scripts
| Command | What it does |
| -------------------------------------------- | ---------------------------------------------------------------------------------- |
| `npm run dev` | Next.js dev server |
| `npm run build` / `npm start` | Production build / serve |
| `npm run typecheck` | `tsc --noEmit` across app, workers and scripts |
| `npm run check:terms` | Forbidden-terms lint (keeps pre-Stellar chain names and bespoke-402 residue out) |
| `npm run check:locales` | Locale completeness check — diffs every registered locale against `messages/en.json`; exits 1 on missing or funded-feature violations |
| `npm run check:locales:strict` | Same as above but also fails on extra (stale) keys not present in the English baseline |
| `npm run test:contracts` | `cargo test --release` over `contracts-soroban` |
| `npm run check:toolchains` | Pinned release toolchain, contract MSRV and CI toolchain matrix agree (no Rust needed) |
| `npm run check:schema-snapshot` | Fail-closed Postgres schema / migration drift check; `-- --write` regenerates the snapshot (no secrets) |
| `npm run workers` | Run all agent workers in parallel (Railway entry point: oracle + market-creator + council + sync + traders) |
| `npm run oracle` | Run only the oracle (settler; optionally `AUTO_CHALLENGE=1`) |
| `npm run market-creator` | Run only the market-creator |
| `npm run council` | Run only the 10-persona Mimir Council worker |
| `npm run sync` | Run the chain-to-Neon sync worker |
| `npm run stellar:keys` | Deployer + oracle keypairs, Friendbot-funded, with a USDC trustline |
| `npm run stellar:usdc` | Testnet USDC faucet helper |
| `npm run stellar:bindings` | Regenerate the contract bindings after a contract change |
| `npm run agents:create-wallets` | Generate 12 local Stellar keypairs (oracle + creator + 10 council) into `.env.local` |
| `npm run agents:fund` | Fund agent accounts from a master seed (the faucets top up the master only) |
| `npm run agents:funding-plan` | Dry-run the funding distribution without sending |
| `npm run agents:balances` | Print oracle + creator + council USDC and XLM balances |
| `npm run deploy:contract` | Build, deploy and initialize the Soroban contracts on Stellar Testnet |
| `npm run verify:deployment` | Check the deployed contract ids, WASM hash and initialized config |
| `npm run verify:artifacts` | Verify / pin Soroban Wasm digests against `deploy/contract-artifacts.manifest.json` (no secrets) |
| `npm run verify:analytics` | Check exported analytics quality; privacy/release runbook: [`docs/ANALYTICS_PRIVACY.md`](./docs/ANALYTICS_PRIVACY.md) |
| `npm run backup:read-index` | Dump the Neon read-index cache to a self-verified checksummed archive (`--out <path>` or stdout; needs `DATABASE_URL`, no seeds) |
| `npm run verify:cache-backup` | Verify a cache-backup archive offline — no `DATABASE_URL`, no network, byte-reproducible |
| `npm run restore:read-index` | Restore a *verified* archive into the Neon read-index and fingerprint-check the result (`--dry-run` to preview) |
| `npm run smoke:onchain` | On-chain smoke test against the live deployment (`:full` adds resolve + squad) |
| `npm run smoke:x402` / `:http` | Payment-scheme smoke against live Testnet / a full HTTP round trip |
| `npm run load:x402` | Offline load test: fixture verification + settle/replay limits |
| `npm run load:rate-limit` | Offline load test: the API rate limiter under mixed traffic |
| `npm run test:node` | Full Node test suite with a versioned compile cache and process isolation |
| `npm run test:node:coverage` | Full Node suite plus a fresh LCOV report at `coverage/node/node-tests.lcov` |
| `npm run test:smoke` | Backwards-compatible alias for the Node test runner |
| `npm run test:research` | Research adapters, categories, SSRF guard, x402 discovery suites |
| `npm run test:baskets` | Basket validation, virtual NAV and high-water fee suites |
| `npm run test:squad` | Squad view and pool suites |
| `npm run test:schema` | Schema backlog + schema snapshot gate suites |
| `npm run test:kill-switches` | Every incident kill switch at its enforcement point, offline |
| `npm run warm:vs-index` | Rebuild the Neon read-index from current on-chain state |
| `npm run seed` / `npm run seed:dry` | Seed demo claims (live / dry-run) |
| `npx tsx scripts/demo-full-cycle.ts` | Full create -> challenge -> settle demo in ~90s |
| `npx tsx scripts/check-claim.ts <id>` | Print a claim's state and deadline |
| `npm run rollback:rehearsal` | Dry-run the full deployment rollback against a deterministic fixture (no secrets needed, runs in CI) |
| `npm run rollback:rehearsal:live` | Same rehearsal against your local `.env.local` (does not touch the chain) |
### Node test workflow
`npm run test:node` is the release-safe full Node suite. It discovers every
`tests/node/*.test.ts` file, keeps Node's process-per-file isolation, and uses a
compile-only cache under `.cache/node-tests`. The cache is keyed by Node,
`package-lock.json`, `tsx`, and the runner; test outcomes are never cached. Use
`npm run test:node -- --no-cache` to force a clean compilation or
`-- --clear-cache` to remove the selected cache before a run.
The child test environment is an explicit allowlist. It does not load
`.env.local` or pass production seeds, RPC credentials, LLM keys, or other
secrets. Database-backed tests remain skipped by default; use
`npm run test:node -- --with-db` only with an isolated test database and local
credentials. This workflow does not write to Stellar, Neon, or any deployment.
`npm run test:node:coverage` runs the same complete suite with fresh spec and
LCOV reporters. A passing run publishes `coverage/node/node-tests.lcov`; a
failing run publishes `coverage/node/node-tests.failed.lcov` and exits nonzero.
CI uploads the report on success or failure, including on a cache miss. The
report contains source paths and coverage data, not environment values or test
secrets. Neither `--test-isolation=none` nor result reruns are part of the
release command.
For a local cache reset, remove `.cache/node-tests`. A rollback is limited to
reverting the runner, package scripts, and CI cache/artifact steps; it does not
require a contract migration, chain action, or deployment rollback.
### CI: fail-closed path filters
CI decides what to run from an allowlist (`.github/workflows/ci.yml`). The
`paths` job classifies every changed path via `scripts/ci/changed-paths.mjs`
(mirrored and under test in `lib/ci/path-scope.ts`), then the `suites` matrix
runs only the tiers (`app`, `agents`, `contracts`, `tests`) the change touches.
The rules that keep it honest:
- **Fail closed.** A path matching no scope is UNKNOWN and fails the job. When
the change set cannot be determined at all (missing event payload, failed
fetch, no merge-base), every tier runs. A green check never hides suites.
- **Adding a scope is a reviewed decision.** New top-level directories, root
files and docs exemptions must be added to both `lib/ci/path-scope.ts` and
`scripts/ci/changed-paths.mjs`; `npm run check:driver-parity` fails when the
two drift, and `tests/node/ci-path-scope.test.ts` pins the behaviour.
- **Doc-only changes** (README, CHANGELOG, LICENSE, `docs/`) are the only changes that
enable nothing.
To preview what CI would run for your branch:
```bash
npm run ci:changed-paths -- --base origin/main
Mimir ships the core claim-market primitive: a creator stakes one side, challengers stake the counter-side, and settlement releases the funded pot to the winning side. The contract already supports odds_mode (pool or fixed), max_challengers, challenger stake sizing, rematches (via parent_id), and private invite links, so the modes below are mostly product policy before deeper contract changes.
| Mode | Status | Rules in one line |
|---|---|---|
| Pool Market | live | Pari-mutuel: challengers share the creator stake proportionally; creator wins everything if right. Rewards contrarian conviction; prices crowd consensus. |
| Duel / 1v1 | live | One creator, one challenger, matched stake, winner takes the two-person pot. max_challengers = 1 with equal stake enforced (DuelNeedsEqualStake), "Accept Duel" CTA, rematch flow. |
| Fixed Odds | contract-ready | Creator guarantees a challenger return multiple backed by creator liquidity; predictable payout before joining. |
| Underdog discovery | product layer | Badges, sorting and upside previews for crowded-side contrarians. |
| Rematch ladder | partial (parent_id) |
Settled claim spawns the next round; social loop for rivalries. |
| Streak scoring | read-index first | Consecutive-win streaks surfaced on profiles before any contract change. |
| Conviction mode | future scoring layer | Scores how early and how strongly you backed a side, not just whether you were right. Kept transparent and secondary to actual payouts. |
Recommended rollout order: improve Pool Market legibility, harden Fixed Odds UI, add Underdog discovery, expand the Rematch Ladder, prototype Streak and Conviction as read-index features, then finish Squad vs Squad — contracts-soroban/mimir-squad already implements two-sided pools with pull payouts, so the remaining work is product surface rather than accounting.
These show up in PR review and shape what we accept:
- Contract state is source of truth. The Postgres read-index is a cache. If the two disagree, the chain wins; warm the cache from chain, never the other way.
- Agent seeds stay in the worker.
S…seeds live only in Railway/worker env (STELLAR_ORACLE_SECRET,CREATOR_SECRET,COUNCIL_*_SECRET). The web server holds publicG…addresses only. New agent writes go throughlib/agent-wallets.ts. BYOA keys never leave the agent owner's infrastructure. - Trust through process, not branding. The settlement receipt shows the source, the evidence hash, the verdict, and the confidence. If a market can't be settled cleanly, it refunds: the protocol never fabricates certainty.
- Fees on profit only. No fee schedule may be able to make a winner receive less than their principal, and refunds are always full.
- Narrow claims over expressive chaos. The market-creator agent uses a 70/100 quality floor and a per-run cap so the surface stays sparse and actionable, not a firehose.
- Legibility over magic. Every async path that takes more than ~5s (LLM call, ledger confirmation) surfaces progress in the UI or the worker logs.
- Refund the ambiguous.
DrawandUnresolvableare first-class verdicts that return stakes. Better to be inconclusive and refund than to be wrong and pay out. - A pull is not a worse push. Challenger settlement, parked payouts and accrued fees are all collected by their owner, because a transaction's ledger-entry footprint cannot fit 100 payouts and because one frozen trustline must not be able to fail everyone else's settlement. Nothing expires, and the last claimant absorbs the dust.
- Strkeys are compared exactly.
G…/C…addresses are case-sensitive base32. Never lowercase one to normalise it — that is the EVM reflex, and in an allowlist written the wrong way round it fails open.
Mimir's money-moving paths are gated by a coverage step that runs in CI and locally without any production secrets.
# Run all node tests (same as CI test:smoke)
npm run test:smoke
# Run the coverage-gated subset and print a summary
npm run test:coverage
# If PASS_SECRET is not set in your environment, set any non-empty value:
PASS_SECRET=test npm run test:coverageDATABASE_URL is not required — lib/paid-revenue.ts falls back to an in-memory ring buffer when the variable is unset, which is the default test path.
npm run test:coverage enforces the following minimums via c8 on each of the 8 money-moving modules:
| Module | Lines | Branches |
|---|---|---|
lib/fees.ts |
80% | 70% |
lib/payout.ts |
80% | 70% |
lib/money.ts |
80% | 70% |
lib/paid-pass.ts |
80% | 70% |
lib/paid-revenue.ts |
80% | 70% |
lib/x402/buyer.ts |
80% | 70% |
lib/agents/registry.ts |
80% | 70% |
lib/agents/spend-permissions.ts |
80% | 70% |
Thresholds are declared in .c8rc at the workspace root. The step fails the build when any threshold is breached.
New test files covering the previously-untested modules live in tests/node/:
money.test.ts—formatUsdc/formatUsdcBareedge casespaid-pass.test.ts— HMAC round-trip, expiry, tamper detectionpaid-revenue.test.ts— in-memory ledger idempotency, ring-buffer eviction, aggregationx402-buyer.test.ts— kill-switch fail-closed,PaymentBudgetExceededproperties
All tests use the Node built-in test runner (node --test). No additional test framework is required.
Update this section in the same PR if you:
- add a new environment variable that the
test:coveragestep requires - rename or remove the
npm run test:coveragecommand - add, remove, or reorder steps in
.github/workflows/ci.yml
AGPL-3.0: see LICENSE.
Mimir is source-available. You can use, study, modify, and share it freely. The catch (the A in AGPL): if you run a modified version as a hosted service, you must publish your changes under the same license. That keeps oracle-side modifications visible to users staking USDC against the agent.
Malformed evidence fails closed. A supplied evidence_hash must be exactly 32 bytes (64 hexadecimal characters, with an optional 0x prefix) or the client rejects it before signing and submission. Omission keeps the existing zero-hash sentinel for compatible callers, while the Soroban ABI independently enforces BytesN<32>. This is a client validation-only rollout: it changes no storage, fees, escrow, payouts, permissions, or secrets; requires no migration or redeployment; and can be rolled back with the client change alone.
