The trading agent that cannot lie about its record.
candor.website · public record export · how to verify me
Candor is an autonomous sports trading agent built for the TxODDS World Cup Hackathon (Trading Tools and Agents track). It ingests TxLINE's live odds and scores firehoses, prices every quoted line with its own model, takes paper positions on a frozen strategy, and, this is the point, produces a track record that is impossible to fake:
- Every position is hashed and committed to Solana mainnet before its outcome exists.
- Every settled position is proven on-chain against the Merkle roots TxODDS anchors,
via the oracle program's validation calls (
validate_stat_v3multiproofs since their 2026-07-13 mainnet promotion,validate_stat_v2as the automatic fallback). Wins and losses are proven identically. - Every number on the public dashboard carries a verify link or is recomputable from the machine-readable record export. You do not have to trust the website, the repo, or us.
Every tipster, signal seller, and trading bot self-reports its results. Screenshots of winners, deleted losers, backdated picks: fake track records are the defining fraud of the industry, and until match truth lived on a chain there was no neutral way to prevent it. TxLINE anchors match truth on Solana. Candor is the first trader built so that its honesty is a protocol rather than a promise.
No real money is involved anywhere. The bankroll is 1,000.00 paper units by design and by rule. Solana mainnet is the trust layer only; the agent never custodies funds and the TxL token is used for data access, never traded.
Documentation: the full five-chapter set lives in
docs/— trust layer · how it trades · parameter tuning · TxLINE integration · architecture
- Documentation
- The honesty protocol
- Verify the record yourself
- The strategy inside
- Architecture
- Determinism and the verification bar
- Exactly how TxLINE is used
- Empirical findings the build surfaced
- Running it yourself
- Repository layout
- What Candor never does
Full specification with the exact byte formats, threat model, and live artifacts:
docs/trust-layer.md. The construction is not specific to this
agent (we call it the Candor protocol; any signal vendor or fund could run the same
cycle). The short version: four mechanisms, all live on mainnet from the agent's own
wallet
(DKdqzAhvYMB3TZFZSM7M6JA3nQqmsjk5W9Smo6vq7xrE).
The moment a position opens, the worker canonicalizes the complete decision into a JSON
payload (schema candor.position.v1: fixture, market key, scope, side, family, price
taken, model and market probabilities, stake, bankroll before, entry score, decision
timestamp, and the strategy hash), hashes it with sha256, and broadcasts a memo
transaction:
candor|v1|commit|<payload sha256>|params:<strategy hash, 16 chars>|prev:<previous commit signature | genesis>
The chain timestamp proves the position existed before the outcome. The prev field
chains every commit to the one before it, so the record is append-only: deleting a losing
position leaves a hole any verifier can see. Commits are strictly serialized so the chain
stays linear, retried on failure, and marked failed visibly if mainnet cannot be reached
(source: src/chain/commit.ts).
A real example, from the live record: position #4 (Under 1.5 first-half goals, Argentina v
Switzerland, World Cup quarterfinal) was committed at 01:01:38 UTC, ninety seconds after
kickoff, hours before the outcome existed:
43P8YQpf….
TxODDS anchors per-match statistics on Solana as Merkle roots. When a match finalises,
Candor grades its positions from the certified period bands, then compiles each
position's exact win condition into a validation call on the oracle program
(9ExbZjAapQww…)
and broadcasts it: validate_stat_v3 multiproofs, with validate_stat_v2 as the
automatic fallback (TxODDS promoted V3 to mainnet mid-tournament on 2026-07-13; Candor
probed every predicate shape through both methods and adopted it the same day — each
proof row records the method that certified it). The proof does not say "the score was
2 to 1"; it says this position's claim evaluated true (or false) against the root
TxODDS committed:
| Market | Compiled predicate |
|---|---|
| Home win (1X2) | goals1 − goals2 > 0 (binary subtract) |
| Draw | goals1 − goals2 == 0 |
| Over 2.5 goals | goals1 + goals2 > 2 (binary add, push handled separately) |
| First-half markets | same predicates on the +1000 period band keys |
| Full-match after extra time | component proof: four equalTo legs pinning the H1 and H2 bands |
Before broadcasting, the worker simulates the proof and requires the simulated verdict to
match its own settlement; a mismatch blocks the broadcast and flags the position
(source: src/chain/proof.ts). Every settled position ends in
exactly one of three states, shown publicly: proven, proof_unavailable (with the
reason), or void. There is no fourth state.
Real examples: position #3's win proven at
BA7FsisU…,
position #4's at
21tPRJdm…
(that one self-healed through the proof retry sweep after the anchoring lag, with no human
involved). During validation we also proved a deliberately false claim is rejected
on-chain; both directions are exercised before anything is trusted.
Every commit carries the hash of the complete strategy parameter set. The parameters were
tuned on recorded matches, frozen before deployment, and the freeze itself was committed
to mainnet as a ceremony transaction:
3xmdHkBG…,
binding hash e8d0d4b6f761e75ceecdb8d7f0ea321d27f188d39db2ba3763f65c776d4842d8. If a
threshold ever moved mid-run, the hashes in the chain would show the seam. The parameter
derivation, including its dead ends, is documented in
docs/params-tuning.md.
Every position the agent opens, and every trade candidate it flags but turns down on
sizing or exposure grounds, is logged with its inputs and reasoning; the routine
evaluations where the model agrees with the market and no candidate arises are not logged.
After each UTC day that logged at least one signal, the worker builds a Merkle tree over
that day's log and commits the root to mainnet
(candor|v1|decisions|<date>|root:…|n:…|prev:…), so no logged signal can be edited,
deleted, or inserted after the day closes. Quiet days produce no root, and each root's
prev chains over them, so one cannot be inserted later. Because every trade is itself a
commit on-chain, a hidden position would show as a commit with no matching signal
(source: src/chain/decisions-root.ts).
You need nothing from us but a Solana RPC. The complete procedure lives at candor.website/verify; the short version:
-
Fetch the record:
GET /api/recordreturns every position with its canonical payload, hashes, signatures, settlement evidence, and the full signal log. -
Recompute a hash and compare it to
payload_hash:node -e "const c=require('crypto');fetch('https://candor.website/api/record').then(r=>r.json()).then(r=>{const p=r.positions[0];console.log(c.createHash('sha256').update(p.payload_canonical).digest('hex')===p.payload_hash?'hash matches':'MISMATCH')})" -
Read the commit transaction (
commit_sig) on any explorer: the memo carries that hash, the frozen params hash, and the previous signature, at a block time before the match ended. -
Read the proof transaction (
proof_sig): a validation call against TxODDS's root returning the position's certified verdict;proof_methodnames the instruction (validate_stat_v3orvalidate_stat_v2).
Or press Verify on any position at candor.website/positions: your own browser recomputes the hash with WebCrypto and reads the commit transaction from a public RPC. This site is deliberately not in the trust path.
The accountability protocol is the invention; the trader inside it is still built fully,
because an honest record of a toy would prove nothing. The complete orientation document
is docs/how-candor-trades.md; the one-paragraph version:
Candor does not predict matches from history or news. It reads the entire quoted board (1X2, over/under, Asian handicap; full match and first half) from TxLINE's StablePrice consensus, and every five seconds fits a Poisson model of the remaining goals implied by the live totals lines, splitting it between the teams against the quoted 1X2. That model then prices every line on the board. When one line's demargined probability disagrees with the model by more than that market bucket's measured noise floor (thresholds derived on recordings and frozen: 2.5 points base, 6.0 for Asian handicap, documented in the tuning paper trail), and survives freshness, liveness, phase, and honest-fill gates, it becomes an entry candidate. A second family watches for sharp movement (per-line z-score jumps) and arms the divergence scan on lines re-quoted after the jump. Stakes are quarter-Kelly, capped at 2% of bankroll, with per-match and concurrency exposure limits. Measurement is deliberately the professional kind: fixed horizon closing line value (an in-play line's last quote restates the outcome, so a 10-minute horizon isolates whether the market moved toward the position while it still traded), Brier calibration against the consensus on the same positions, drawdown, and exposure statistics. All of it is displayed, including when it is unflattering; negative CLV on winning positions is reported in the same register as the wins.
Two small services and a database, deliberately boring. The full chapter, including the
bootstrap order, every runtime loop, warmup, the data model, and the failure-mode table:
docs/architecture.md.
flowchart LR
TX[TxLINE mainnet<br/>scores + odds SSE] --> W
subgraph Railway
W[worker<br/>ingest · model · signals · ledger] --> DB[(Neon Postgres)]
D[dashboard<br/>Next.js, read-only role] --> DB
end
W -->|memo commits · validate_stat proofs · daily roots| SOL[Solana mainnet]
D --- U[candor.website]
| Piece | What it does | Where |
|---|---|---|
| Worker | One long-running Node process: fixture discovery, serialized scores fold, batched odds book, 5-second evaluation cadence, paper ledger, commit chain, settlement, proofs, retry sweeps, daily decisions roots, health endpoint. Restart-safe: warms up from the database and snapshots. | src/worker/ |
| Dashboard | Public, read-only. Connects through a SELECT-only Postgres role (candor_reader), so the public surface cannot write the record even if compromised. |
dashboard/ |
| Database | Nine tables: fixtures, match_state, odds_latest, odds_history, signals, positions, settlements, proofs, agent_state. | src/db/schema.sql |
| Chain | SPL Memo for commits and roots; TxODDS txoracle for proofs; ~5,000 to 8,000 lamports per transaction from the agent's own wallet. |
src/chain/ |
Operational rules the deployment enforces: exactly one writer (the Railway worker; local workers are killed at cutover and stay dead), params frozen (a mid-run bugfix may change code, which is visible as an epoch in the commit stream, but never parameters), and the dashboard redeployable all day without touching the trader.
The judging rubric asks for deterministic, defensible logic. Candor makes that checkable rather than asserted:
- Byte-identical replays:
tests/determinism.tsruns full recorded matches through the exact production stack, twice in-process and once in a fresh process, and requires byte-identical digests of every decision and every accountability artifact: canonical payloads, payload hashes, commit memo bodies, and the bankroll trajectory. Same recording in, same on-chain bytes out. - Grader parity: the production settlement grader and the replay harness grader are asserted to agree on every entry, so tuning evidence and live settlement can never silently diverge.
- Frozen-replay pins: the frozen parameters must keep reproducing the documented tuning-sweep results on the recordings (3 entries, 2 won 1 lost, +9.2 units), including reproducing the two entries the live agent actually took, at the same lines and prices.
- True and false cases on-chain: every proof mechanism was validated in both directions on mainnet before being trusted (a true claim about a real final returned true, a false claim returned false), including the extra-time component-proof path.
- In-browser verifier: the dashboard's verify button re-derives the record in the visitor's own browser, keeping this site out of its own trust path.
Match recordings are captured from the live streams by
scripts tooling and are not part of the repository; the
determinism suite runs over whichever recordings exist locally.
Candor consumes the TxLINE mainnet real-time subscription (World Cup and International
Friendlies service level), plus the Solana programs it anchors to. The full chapter,
including stream operational behavior, payload anatomy, and the corrected period-band
table: docs/txline-integration.md. Every surface, from
src/txline/client.ts:
| Surface | Use |
|---|---|
POST /auth/guest/start |
JWT issue and re-issue when the session token goes stale |
GET /api/fixtures/snapshot |
fixture discovery every 10 minutes; new fixtures (semifinals, final) appear automatically |
GET /api/scores/stream (SSE) |
the scores firehose: phases, clock, per-period stat bands, game_finalised |
GET /api/odds/stream (SSE) |
the StablePrice odds firehose: demargined consensus per line, pre-match and in-play |
GET /api/scores/snapshot/{id} + GET /api/odds/snapshot/{id} |
restart-safe warmup re-seeds live state; the scores snapshot also seeds the match recorder |
GET /api/scores/stat-validation-v3 |
settlement-grade Merkle multiproof payloads for validate_stat_v3 (primary since its 2026-07-13 mainnet promotion) |
GET /api/scores/stat-validation |
proof payloads for the validate_stat_v2 fallback path |
validate_stat_v3 / validate_stat_v2 (on-chain) |
one call certifies a position's entire win condition against TxODDS's root (1.4M CU); V3 first, V2 on fallback, the method recorded per proof |
| SPL Memo (on-chain) | position commits and daily decisions roots |
Stream handling detail that mattered in production: both firehoses are deflate-encoded
with ~20 second heartbeats; the worker runs a 90-second liveness watchdog, reconnects with
backoff, dedupes scores by (FixtureId, Id, Seq) and odds by MessageId, and folds stats
as latest-value because VAR retractions genuinely roll bands back mid-match.
The V3 validation path (stat-validation-v3 + validate_stat_v3 multiproofs) was
rehearsed end to end on devnet with this same wallet on July 12: true and false cases
and a real broadcast. When TxODDS promoted it to mainnet the next day, the same-day
probe re-verified every predicate shape the win-condition compiler can emit, in both
directions and through both methods, and the proof layer adopted V3 as primary with
validate_stat_v2 as the automatic fallback.
Things the docs did not say, established from recordings and live matches, and fed back to TxODDS as a 14-item evidence-backed feedback report (curated separately in the submission):
- The soccer period-band table is wrong in the docs. Observed on
game_finalisedrecords:+1000= first half,+2000= halftime cumulative,+3000= second half,+4000= first extra-time period,+7000= extra-time cumulative. Settling from the documented table silently corrupts second-half and full-match outcomes. - In-running Asian handicap lines are remaining-goals handicaps, never full-match margins. The two readings coincide at level scores, which is exactly why it is easy to ship wrong.
- Quarter lines never carry demargined probabilities (
Pct: ["NA","NA"]), so they cannot be signal targets. - Validation payloads lag finalisation (Merkle batch anchoring) and the lag presents
as a bare 404;
game_finaliseditself follows the final whistle by several minutes. The worker's proof retry sweep exists for precisely this window. - VAR rollbacks are real: a disallowed goal rolled the stat bands back three minutes later. Stats must be folded latest-value, never monotonically.
.view()simulations need a funded fee payer, signed memos cost ~140k CU, and multi-leg validations need the full 1.4M CU limit.
Prerequisites: Node 20+, a Postgres database, a funded Solana wallet (mainnet lamports for commits and proofs), and TxLINE API access (the World Cup tiers are free during the event; see TxODDS's documentation).
git clone https://github.com/AngryPacifist/Candor
cd Candor && npm install
cp .env.example .env # fill in the values below
npm run migrate # applies src/db/schema.sql (idempotent)
npm run worker # the whole agent: ingest, trade, commit, settle, prove
cd dashboard && npm install && npm run dev # public dashboard on :3100| Variable | Meaning |
|---|---|
TXLINE_API_ORIGIN |
https://txline.txodds.com |
TXLINE_API_TOKEN |
long-lived TxLINE API token (from the subscribe + activate flow) |
TXLINE_JWT |
bootstrap session JWT; the client re-issues automatically |
SOLANA_RPC_URL |
mainnet RPC (commits, proofs, balance guard) |
SOLANA_DEVNET_RPC_URL |
optional; V3 rehearsal tooling |
AGENT_KEYPAIR_PATH / AGENT_KEYPAIR_JSON |
the agent wallet (file path locally; raw JSON array on hosted platforms) |
DATABASE_URL |
Postgres owner string for the worker; give the dashboard a read-only role instead |
PUBLIC_URL |
the dashboard's public origin |
Tests: npm run typecheck in both packages, and npx tsx tests/determinism.ts (needs
local recordings). The worker exposes GET /health for platform checks and refuses to
start with named errors if the environment is incomplete.
src/
txline/ REST + SSE client (reconnect, watchdog, JWT re-issue), record types
ingest/ fixture discovery, serialized scores fold, batched odds book
model/ market parsing, the remaining-goals Poisson fair-price engine
strategy/ frozen params (hashed), divergence + movement signals, Kelly sizing
ledger/ paper ledger, exposure gates, settlement from certified period bands
chain/ memo commit chain, validate_stat proofs (V3 multiproof, V2 fallback), daily decisions roots
replay/ deterministic replay of full recordings through the production stack
worker/ bootstrap (health-first, named env validation) + the autonomy loop
db/ schema (nine tables) + migration
dashboard/ public read-only Next.js app: record, receipts, charts, verifier
docs/ trust-layer.md · txline-integration.md · architecture.md ·
how-candor-trades.md (the strategy) · params-tuning.md (the derivation)
tests/ the verification bar: determinism suite, fair-price noise floor,
signal replay, settlement validation, on-chain proof validation
(requirements per test: tests/README.md)
- Never touches real money: no custody, no wagers, no payouts; paper units only.
- Never trades the TxL token: it is data access, nothing else.
- Never edits the past: commits are hash-chained, the strategy hash is frozen and ceremony-anchored, settlements carry evidence, and degraded states are shown as degraded rather than hidden.
- Never asks to be believed. That is the whole point.
Built solo for the TxODDS World Cup Hackathon, Trading Tools and Agents track · data by TxLINE, anchored on Solana · live at candor.website