Autonomous DeAI forecasting and market making for DreamDEX Event Contracts on Somnia.
Built for the Somnia × DreamDEX Event Contracts Hackathon · Apache-2.0
Live on Somnia testnet: usevaticr.xyz
The five things worth opening first, in order of how much they prove:
| The model is measurably right | Backtest evidence - 900 forecasts, 300 windows, Brier 0.15522 against a coin flip's 0.25. Skill +0.3791. Lookahead-free and frozen; the harness is agents/backtest.py. |
| It ran onchain, before the fact | Onchain proof - a forecast committed to the registry 318 seconds before its window settled, and a post-only order that rested on the real book inside the touch. Every step links to its transaction on the Shannon explorer. |
| It checks its own work | The settlement audit recomputes every settled window from the public oracle feed and compares it to the onchain winner. It is the part of the app that could embarrass us, which is why it is in the app. |
| We went past the happy path | SDK feedback report - eight substantive findings plus a list of smaller ones, all hit in practice and verified against live testnet data, including the settlement reference being the EMA rather than spot, which is undocumented and quietly changes what a correct price is. |
| It is not a demo shell | 114 tests - 75 Python, 32 TypeScript, 7 Solidity. npm test runs all of them. |
Deploying it yourself: DEPLOY.md. Recording the walkthrough: DEMO.md. The submission itself: SUBMISSION.md.
Running live, all three services on one host behind Caddy: the Next.js app, the Python forecasting API on loopback, and the trading bot. One idempotent script stands the whole thing up and is safe on a server that already has tenants - it joins whichever web server owns port 80 rather than fighting it, and rolls its own config back out if the config test fails.
A DreamDEX Event Contract asks one question: will this window close at or above the price it opened at? That makes the fair value of a YES token a genuine probability - and a probability can be derived rather than guessed.
Vaticr derives it, trades it, and then proves whether it was any good.
- Prior from the price process. Where the window sits relative to its own open, given time remaining and measured volatility.
- Posterior from the news. Live headlines scored for directional impact and folded in as log-likelihood ratios - decayed by age, credibility, and how much of the window is left.
- Traded through the official Bot Kit. Takes when the posterior clears the touch; otherwise rests a two-sided mint-a-pair quote that needs zero inventory.
- Audited afterwards. Every settlement is independently recomputed from the public oracle feed, and every forecast is Brier-scored against what actually happened.
Not a market factory. DreamDEX Event Contracts are protocol-created rolling BTC/ETH windows, and they settle themselves via the OracleHub - there is no permissionless market creation and no news-driven resolution. Vaticr works with that design rather than against it. The reasoning is in docs/ARCHITECTURE.md.
git clone https://github.com/mrnetwork0001/Vaticr.git && cd Vaticr
npm install # workspace: bot + vendored Bot Kit ec-core
python3.11 -m venv .venv # the tooling looks for ./.venv first
./.venv/bin/pip install -r requirements.txt
cp .env.example .env # works as-is; DRY_RUN=true by default
npm run bot:start # one command: starts the brain, then the botThat is the whole setup. bot:start brings up the Python intelligence layer,
waits for the first news scan, then runs the trading loop - in dry run,
logging every order it would place and sending nothing.
The virtualenv is not optional in practice: scripts/start.mjs and the api /
test:agents npm scripts all prefer ./.venv/bin/python and only fall back to
whatever python3 is on PATH. Any Python 3.11+ interpreter works; put it at
./.venv and everything finds it.
Check everything first:
npm run doctor # venue scope, live markets, API, wallet, balancesTo trade for real, set a funded key and turn off the safety:
# .env
DRY_RUN=false
PRIVATE_KEY=0x...Watch it on the dashboard:
npm run dev # http://localhost:3000If port 8787 is taken: VATICR_API_PORT=8799 npm run bot:start (and set
VATICR_API_URL=http://127.0.0.1:8799 so the bot and the dashboard follow).
Dry run needs nothing. Trading for real on Somnia testnet needs two assets, and only one of them is your problem:
1. STT for gas - you fetch this. Somnia's Shannon testnet (chainId 50312) pays gas in STT.
| Faucet | https://testnet.somnia.network/ |
| Explorer | https://shannon-explorer.somnia.network/ |
| RPC | https://api.infra.testnet.somnia.network |
Alternate faucets if that one is dry: Google Cloud, Stakely, thirdweb.
Get more than a faucet gives you. The SDK signs with a fixed 10,000,000 gas
ceiling at 60 gwei, and Somnia reserves gas_limit × maxFeePerGas up front, so
a signer needs 0.6 STT free per in-flight write regardless of the ~0.008 the
transaction actually costs. Measured: at 0.533 STT every order failed at the
approve; at 0.833 the same wallet placed four orders across two markets. Worse,
the rejection arrives as "Missing or invalid parameters", which sends you
looking at your payload rather than your balance. Aim for 2 STT. This is
finding 7.
2. tUSDC collateral - the Bot Kit fetches this for you. Test collateral is
0x70a86D8842FB63C4Ad2b7cdddF530eBf1BB25d8E (6 decimals) and it exposes a public
faucet(uint256). ec-core's seedInventory() calls it automatically whenever
the signer's balance drops below 1,000 tUSDC, on any non-mainnet network. You do
not need to do anything - but it does need gas, which is why STT comes first.
Set FAUCET_ENABLED=false to turn that off.
Through the app instead. A wallet that arrives at
usevaticr.xyz/dashboard with nothing in it is
told so: the app offers to add Somnia if the wallet has never heard of it, then
names what is still missing. Gas links out to the faucets above, because we
cannot mint it. Collateral is one button, because the testnet token's
faucet(uint256) is public. The panel removes itself once both are present.
Confirm both with npm run doctor. It prints the wallet's native and collateral
balances and fails on either being zero - and on gas being merely thin, since
the SDK signs with a fixed gas ceiling rather than estimating, so a balance that
looks non-zero can still be rejected at send time. On mainnet there is no
faucet: collateral is real USDso and the bot warns and skips rather than
minting.
cycle 4: 8 live market(s), 10 forecast(s)
BTC-0-01SEP26-0630/tUSDC book=[0.096/0.118] prior=0.186 post=0.186 news=-0.003(2) net=0.0 -> take_yes
posterior 0.186 clears ask 0.118 by 0.068
ETH-0-01SEP26-0700/tUSDC book=[0.065/0.086] prior=0.101 post=0.101 net=0.0 -> quote
no takeable edge (mid 0.075) - quoting around 0.101
QUOTE BUY_YES 5 @ 0.071 resting id=...
QUOTE BUY_NO 5 @ 0.869 resting id=...
ETH-0-01SEP26-0620/tUSDC: skip - only 41s left (need 120s)
And the settlement audit, recomputed independently from the oracle feed:
market chain derived open close margin verdict
ETH 300s @1788270000 down down 2443.07 2435.90 -29.33bp match
BTC 300s @1788270000 down down 77809.31 77595.24 -27.51bp match
BTC 900s @1788269400 up up 77783.91 77863.42 +10.22bp match
ETH 300s @1788269100 down up 2439.46 2439.58 +0.48bp inconclusive
11/12 settlements independently verified
1 inconclusive - decided by under 1.0bp, finer than an off-chain
reconstruction can resolve
receipt: https://dev.oracle.somnia.host/questions/48402?view=graph
That last row is the interesting one. The oracle settles on its own sampled tick; Vaticr recovers the reference from the public feed by timestamp, and on a window that closed 0.005% from its open those can disagree. Rather than call that a failed settlement, the audit reports it as inconclusive - our resolution ran out, the chain is not wrong. Over twenty settlements there were zero genuine mismatches.
The project's claim is that an event contract's probability can be derived.
That is testable, so it is tested. Every input is public and historical, so
npm run backtest replays settled windows the model never saw.
The run below is frozen in docs/evidence/backtest-2026-09-04.json,
produced by npm run backtest -- --limit 300 --json. Re-running it will not
reproduce these exact figures - it replays a rolling window of recent
settlements, so the sample moves every day. The frozen file is what the numbers
below quote.
sample 900 forecasts across 300 settled windows
(3 decision points each, at 25% / 50% / 75% elapsed)
span 2026-09-04 00:00 -> 2026-09-04 09:05 UTC
Brier 0.15522 (coin flip 0.25)
skill +0.3791 1 - Brier/0.25
accuracy 0.7622
log loss 0.46816 (coin flip 0.69315)
by time elapsed n Brier skill accuracy
25% into window 300 0.2046 +0.1816 0.6733
50% into window 300 0.16327 +0.3469 0.7533
75% into window 300 0.09779 +0.6088 0.86
Skill rises as the window closes - +0.1816 a quarter of the way in, +0.6088 three-quarters in. That is the signature of a model reading the price process rather than fitting noise: information accumulates and the posterior sharpens with it.
Three things worth disclosing, because being asked about them is worse than volunteering them:
- No lookahead. Volatility and level at each decision point use only ticks at or before that instant. All 900 cases assert it, and 120 are re-run against a history physically truncated at the decision, so the assertion is not vacuous. A lookahead bug is the classic way a backtest lies.
- Prior only. The headline layer is excluded, because a historical scout window cannot be reconstructed without leaking the future. This measures the price-process prior alone; the news layer's contribution is unproven.
- The edge is concentrated in the short windows - where the bot actually trades. 300s scores +0.3865 over n=597, while the long windows are thin and closer to a coin flip on small samples. The frozen JSON carries the full per-window breakdown.
| Command | What it does |
|---|---|
npm run bot:start |
One command. Intelligence layer + trading bot. |
npm run bot:dry |
Same, with DRY_RUN=true forced regardless of .env. |
npm run doctor |
Preflight: network, module bytecode, venue, markets, API, signer, balances. |
npm run dev |
Next.js 14 dashboard on :3000. |
npm run api |
Intelligence layer alone (uvicorn on VATICR_API_PORT, default 8787). |
npm run bot:only |
Trading loop alone - assumes the API is already up. |
npm run backstop |
Poke or void any window stuck past settlement. |
npm test |
17 engine property tests + 7 Solidity tests. |
npm run typecheck |
tsc --noEmit over the app and the bot workspace. |
npm run compile |
Hardhat compile. |
npm run deploy:registry |
Deploy the forecast registry to Somnia testnet. |
Agents can also be driven directly:
./.venv/bin/python -m agents.scout # scan feeds, print scored headlines
./.venv/bin/python -m agents.resolver # audit settlements, print calibrationagents.resolver takes --venue <venueId>, --limit <n>, and --watch
with --interval <sec> to keep sweeping.
agents/ Python 3.11 - read-only. The brain.
scout.py headlines → directional evidence
lexicon.py deterministic scorer (no key required)
llm.py optional Claude classifier - scores surprise, not keywords
pricing.py the Bayesian engine: GBM prior + log-odds evidence
resolver.py settlement audit · Brier scoring · backstops
somnia.py markets indexer + oracle price feed
server.py FastAPI surface the bot polls
store.py forecast commitments (.vaticr/forecasts.jsonl)
bot/src/ TypeScript - every onchain write.
runner.ts the trading loop (npm run bot:start)
strategy.ts take-vs-quote, mint-a-pair levels, inventory caps
backstop.ts pokeOracle / voidExpired
doctor.ts preflight
registry.ts onchain forecast commitments
signal.ts typed client for the Python API
contracts/ VaticrForecastRegistry.sol - append-only, no owner
tests/ Python engine property tests
test/ Solidity tests (hardhat)
app/ Next.js 14. Server components read the chain; the client signs.
page.tsx landing
dashboard/ the working surface: markets, positions, evidence, audit, calibration
privacy/ terms/ what the app stores, and what it does not promise
components/wallet/ connect, chain guard, balances, first-run funding
components/trade/ ticket, positions, claims
api/book/ order-book tops, read from the chain rather than the indexer
api/vaticr/ server-side proxy to the Python layer
video/ Remotion composition for the demo film - see video/README.md
vendor/ec-core dreamDEX Bot Kit ec-core, vendored verbatim (MIT) - see SDK feedback
docs/ ARCHITECTURE.md · API.md · SDK_FEEDBACK.md · VIDEO_SCRIPT.md
evidence/ (frozen backtest) · deck/ (the presentation)
DEMO.md runbook for the demo recording
DEPLOY.md one VPS: web app, forecasting API and bot behind Caddy
SUBMISSION.md the hackathon entry, mapped to the judging criteria
Mint-a-pair is why a small bot can make real markets here. On a binary book,
Buy YES × Buy NO needs no seller - the pool mints a fresh pair for two
opposite-side buyers. So a resting Buy YES @ p−δ plus Buy NO @ (1−p)−δ is a
complete two-sided quote with no inventory and no counterparty maker.
Vaticr never sells, which means the only risk it carries is its net imbalance.
Settlement resolves against the EMA, not spot. This is undocumented, and it
matters: over eight consecutive settlements measured on 4 September 2026, mark reproduced the onchain
winner 8/8 while spot managed 6/8 - disagreeing exactly on the near-the-money
windows that are most worth trading. Vaticr prices the level off mark and
measures volatility off spot. That finding and seven others are written up in
docs/SDK_FEEDBACK.md.
npm test- 114 tests - 75 Python (engine, news scorer, store, resolver, indexer clients), 32 TypeScript (the trading decision layer), 7 Solidity (the registry). The engine's property tests include Monte-Carlo recovery of a known volatility, and a regression for the EMA-smoothing bug that drove live priors to 0.0000.
- 7 Solidity tests on the registry - append-only, no late commitments, agents independent, pagination safe past the end.
Run either half alone with npm run test:agents or npm run test:contracts.
npm test covers those two. Four further Python suites are not yet wired into
it and are run directly:
./.venv/bin/python -m tests.test_lexicon # 15 - the deterministic scorer
./.venv/bin/python -m tests.test_resolver # 17 - audit verdicts, Brier, backstops
./.venv/bin/python -m tests.test_somnia # 15 - the two GraphQL readers
./.venv/bin/python -m tests.test_store # 11 - commitment durabilityThe bot's decision layer has 32 vitest tests on top of that, also outside
npm test:
npm run test -w @vaticr/bot # take-vs-quote, touch pricing, inventory capsVaticrForecastRegistry is live on Somnia Shannon testnet:
0x3D04ff026A4Dc553a2ae9071dbc238a40D24b27A
Vaticr is not a dry-run demo. One command - npm run go-live -- --send - put the
whole loop on testnet, and every step is independently verifiable:
| Step | Transaction |
|---|---|
| Registry deployed | 0x34aacd003fee407156… |
| tUSDC minted from the public faucet | 0x7f977e6137bb8f802a… |
| Forecast committed before settlement | 0xcaf0964062a26a2dd3… |
| Real order resting on the book | 0x5a6afb0c19e85bdc40… |
Read the commitment back off the chain and it still says what it said at the
time: posterior 73.26% on
ETH-0-02SEP26-0330/tUSDC, stamped 318 seconds before that window expired.
That is the entire point of the registry - the forecast was public and immutable
while the outcome was still unknown, so the track record cannot be edited after
the fact.
The order was a post-only BUY_YES 1 @ 0.713 against a book
showing 0.852/0.876, deliberately resting inside the touch rather than crossing.
Collateral escrow confirms it landed: the account holds 9999.287 tUSDC, exactly
10000 minted minus the 0.713 the resting order locked up.
Account activity: 0xEac2828E82…
Vaticr trades the DreamDEX protocol contracts, which it does not own:
| Contract | Address (Somnia testnet, 50312) |
|---|---|
BinaryMarketsModule |
0x3ecC694Cef705358864a646142ac17A90E29e388 |
MarketsCore |
0x2802504314685D89bF6C992CA5a8e7cC78bc0294 |
BinarySettlement |
0xbF4a49e0Dfd092e5FBE8E5761064C49533e6Ed23 |
OutcomeToken6909 |
0xB52c5934113Af5c0Bb20eb3C72290C8215f755b9 |
OracleHub |
0xe40db387cC98601Dd11bd634fF2f3AD5686dE32b |
| tUSDC (testnet collateral, 6 dp) | 0x70a86D8842FB63C4Ad2b7cdddF530eBf1BB25d8E |
These come from vendor/ec-core/src/addresses.ts; npm run doctor verifies the
module still has bytecode at that address, because a stale deployment map is the
most common way a working bot goes quiet.
DRY_RUN=true is the default and prints every order it would send. Before
trading real funds: run npm run doctor, use a hot key holding only what you can
lose, and consider the Bot Kit's
session keys
so the trading key cannot withdraw. Nothing here is financial advice.
Apache-2.0. vendor/ec-core is MIT, © DreamDEX S.A. - vendored verbatim with
its license and provenance intact (NOTICE).