hanko
Hanko turns crypto market chatter into a sized position, or an honest
refusal, with the evidence attached, and can prove that same decision
byte for byte, years later.
Point it at a token and the voices you trust, on X, Telegram or Bluesky. It
reads what each one actually said, checks whether they're independent voices
or one thesis repeated three times, and decides:
Sample output: real engine, synthetic inputs. voice_alpha/voice_beta/voice_gamma and TOKENA are fictional test fixtures. None of them is a real account or token.
Run that same input again in a year and it produces the identical decision
id, because nothing about deciding touches the network, a clock, or a model.
It only ever reads what was actually stored. That is the whole project: "prove it" is a command you can run.
Judging? Start at /judges: every line of
the published rubric, mapped to a page or command you can check yourself.
tryhanko.xyz has the pitch and a sample
receipt (synthetic inputs, labelled). /try runs a piece of it, live,
from your browser, no install. Full technical detail is at /docs.
[The landing page: the sample receipt above, typed out by the real engine against synthetic sample inputs]
RYO-CHAN Hackathon 2026 submission, three tracks. Track 1 (Autonomous
Agents) is the agent above, built to the Multi-KOL Narrative Agent spotlight.
Track 3 (New Skills) is exit_liquidity:
what it costs to get out of a position, from an executable venue quote where
one exists, in RYO's own response envelope. It is also the size gate the agent
runs before committing. Track 2 (Dashboards & Interfaces) is three screens:
the workbench, the
failure drill and the
Echo Index.
What it has shown, live
- A ledger that grows every three hours, with no key. A scheduled
workflow reads public Telegram, Bluesky and news voices on BTC, ETH, SOL
and BONK, decides, seals the run with its replay bundle, stamps it on
OpenTimestamps and commits it to
live/. Every decision is graded
72 hours later against the conditions it wrote down first.
/ledger shows what changed since the
last run, most important first (D-20).
- Graded against its own conditions, including when it was wrong. The
first 32 scheduled decisions came due on 2026-10-27 and 28: every ENTER
held (7 of 7, realised returns between -0.9% and +1.1%), and 7 of 25
refusals were proved wrong because a voice they said would flip them
arrived. Those reviews also exposed two grading bugs, fixed in D-21.
- A sealed live ENTER that anyone can re-derive. BTC, 2026-10-23,
runs/run_836b976720f63dc0.json. The manifest lists every watchlist entry,
including the PASS and both ABSTAINs. It is anchored in Bitcoin block
972,817, mined 35 minutes after the seal, and hanko verify-run replays
all four decisions from the committed bundle with no network and no key.
(Until 2026-10-24 that bundle was not actually in the repository; D-19.)
- A measured exit cost. BONK, $50,000: the Jupiter quote
says 2.3%. The skill's own constant-product model said 7.2%, and the report prints the 3.1x gap.
- A study that audited the agent. The Echo Index
ran Hanko's echo check over 2,094 real posts from 15 crypto channels. Echo
inflation was 1.005x, published as found. First, though, it found that
Hanko's own detector missed verbatim copies padded with an ad or a footer
(D-14, fixed).
- Nine failures injected on purpose. RYO down, every source down, a
rate-limited quote venue, the model down, a prompt injection the model
actually obeyed, tampered evidence, an edited sealed run, a source dying
mid-sweep, a restart. All nine hold, computed live on
/drill. The restart scenario broke the
first time it ran (D-18, fixed).
A 判子 is the seal a person stamps on a document to commit to it. The mark
goes on before the outcome is known and can't be taken back, which is exactly
what this agent does with every decision it makes: sizes it, commits to it,
and states in advance what would prove it wrong.
Submission
Contents
Verify it yourself in 60 seconds
No live credential, no account, no network. Every number below regenerates
from synthetic fixtures committed in this repository (fictional sample voices and a fictional token, written for testing):
Who this is for
Builders and traders who want an agent's reasoning to be auditable rather
than taken on faith. Someone who has been burned by a bot that says
"strong buy signal" with nothing behind it, and wants to see the evidence,
the falsifiers, and the honest gaps instead. Also the judges reviewing this
submission, who can replay any decision from the exact stored bytes it saw.
Not built for anyone who wants the agent to execute trades. It doesn't, and
by the hackathon's own rules, none of the RYO tools it's built on can either.
Everything here researches and sizes a position; nothing here moves funds.
Explore without running anything
The one rule
The agent only ever sees the output of parse(). Because parse() is pure and
payloads are content-addressed, any past decision can be re-derived from stored
bytes and must reach the same verdict. Put a network call inside parse() and
the reasoning trail stops being reproducible, and that property is what the
whole submission rests on.
Layout
Full tree:
Why it is built this way
Sources are interchangeable. The agent never learns where a piece of
evidence came from. Swapping X for Telegram, or Grok for a direct API, changes
one adapter and nothing downstream. If x_search is deprecated or priced out on
day 9, nothing else moves.
That swap is a real option. X's own API
returns structured posts directly, with no model paraphrasing a prose summary
in between, at the cost of a separate paid tier and a different credential
than the one this build already has. Worth doing later; not worth the rewrite
risk this close to submission when the adapter boundary already makes it
cheap whenever it happens.
Failure is recorded as data. A source that was asked and did not
answer produces a FAILED snapshot with the reason attached. An adapter that
raises produces one too. Silence is the single outcome this store cannot
represent, which is what lets position sizing respond honestly to missing evidence.
Three different kinds of nothing stay distinct. FAILED (the source did not
answer), OK with zero items (nobody posted), and PayloadShapeError (bytes
arrived that we cannot read) are separate facts. Collapsing them would let a
schema change look like a quiet market.
Coverage is never overclaimed. x_search is a search tool. It cannot list a full timeline, so its coverage is UNKNOWN by construction. Claiming COMPLETE is
precisely the lie that would inflate a convergence count across KOLs.
Collect once, develop free. Real snapshots are captured once and everything
afterwards runs against stored bytes. Development and CI cost nothing and hit no
rate limit. At an hourly sweep over 20 handles, live collection runs about
$0.005/call, roughly $25 to $35 for the whole build.
Use
Offline, against a fixture:
XAI_API_KEY must be set for live X collection. Without it the adapter records
a FAILED snapshot saying so and carries on.
Status
352 tests, all offline, ~20s. Covered at the evidence layer: canonical form and
address stability, payload deduplication, tamper detection, adapter-version
mismatch on replay, replay determinism, evidence identity pinned to bytes and
position, and each failure mode (rate limit, partial window, empty result,
raising adapter, malformed feed, unreadable payload). At the decision layer:
echo demotion, PASS vs ABSTAIN separation, mechanical size reduction under
missing safety data / a failed source / stale evidence, falsifier generation for
both entries and refusals, ledger tamper detection, and end-to-end reproduction
of a stored decision from stored bytes. At the review layer: falsifier checking,
profit not rescuing a falsified thesis, uncheckable metrics staying unresolved,
inconclusive reviews excluded from every rate, echoes earning neither credit nor
blame, calibration, and the refusal to re-review a decision.
At the MCP layer: the JSON-RPC handshake, session handling, SSE decoding, every
transport failure mode, and a test asserting that facts extracted from an MCP
payload are identical to facts extracted from a REST payload.
Confirmed against the live API, 28 Aug 2026. x_search does not return
structured post objects. It returns the model's prose rendering of the posts
plus an array of URL citations. Prose cannot be attributed to a specific post,
so the adapter requests a JSON schema and then verifies every returned post id
against the tool's own citations: cited posts are kept, uncited posts are
dropped. That check proves a post exists and that the tool saw it; it does not
prove the model transcribed the text or timestamp faithfully, so those fields
are flagged as model-transcribed on every item.
Confirmed against the live RYO MCP server, 5 Sep 2026. hanko tools,
hanko facts SOL, and hanko decide have all run against the real endpoint. Three things that changed once the guessing stopped:
The catalog has six tools. The hackathon's public tool list names seven. check_safety and supported_tokens are not on the authenticated
catalog; monitor_market_sentiment_shift is, and wasn't guessed at all.
There is no numeric safety score anywhere in the six, so safety_score on
MarketFacts now stays None every time.
Argument keys are symbol (analyze_token, deep_analysis) and symbols
as one comma-separated string (compare_tokens). Both transports were first built guessing token and are now fixed to match.
The response is {"tool": ..., "result": {envelope}, "latency_ms": ...}
over REST, and the envelope's own status (ok / partial /
unavailable) and data_mode (live / mixed / simulated / unknown)
are what _degraded() now reads. A response that admits to being
simulated is treated exactly like a partial one, since simulated data
presented as live is the one fabrication the platform says it never does.
Correction, 22 Sep 2026. A routine hanko audit crashed outright with
PayloadShapeError: no message item in the response output. The cause was
a labelling bug: a decision built from local fixture data had been tagged
with the live X source's own id, so replay handed the real adapter bytes it was never built to
read. Every existing replay test had missed it, because each one supplied
its own resolver that returned a fixture unconditionally rather than going
through the same lookup hanko audit actually uses. Full write-up, the
fix, and the regression test that proves it, in
DECISIONS.md D-07.
Not yet shown: a calibration curve from real outcomes, a live ENTER, and a
real priced pool. Stated plainly, with why, in
LIMITATIONS.md.
exit_liquidity, the new Track 3 skill
The six real tools all answer some version of is this worth entering?
Nothing answers can this position be closed, and what does closing it cost?
That's the number that turns research into a trade. A token can clear every
measured signal and still be a trap if exiting the position moves the price
double digits.
Live output from 2026-10-23, recorded while RYO's market tools were down. RYO
has never published pool depth, so version 2.0 asks two keyless venues.
DexScreener supplies pools for the token's canonical contract: never searched
by symbol, because that returns impostor pools. Jupiter supplies an executable
quote for the exact size. A measured quote outranks the model, and the
model's error is printed beside it. Where neither venue answers, cost fields
are null, never zero. Every answer is RYO's public response envelope, also
served on RYO's own REST route shape (POST /api/tools/exit_liquidity/call)
and as an MCP server (/api/mcp).
Full model, honesty conventions, and test coverage in
hanko/skills/exit_liquidity/README.md.
Runnable live, against the real platform, at
tryhanko.xyz/try, restricted to
a few tokens and a capped size, so a public page can't turn into an unbounded
tap on a live credential.
Claude Skill
.claude/skills/hanko-exit-liquidity/SKILL.md
packages this exact command as a Claude Skill: point Claude Code at this repo
and it can answer "what does it cost to exit this position" directly, scoped
by an explicit rule that every number in the answer has to come from the
command's own output, never invented or rounded by the model. The same
honesty convention the tool enforces on itself, extended to whoever is
running it.
Decision records
A record states, at the moment of the decision: what was concluded, on what
evidence, by what reasoning, what was missing, and what would prove it wrong.
The engine is pure. decide() reads its arguments and nothing else: no
clock, no network, no model call, no randomness. Interpretation is the one
subjective step, so it happens upstream and is frozen into the record. Put an
LLM in the decision path and replay proves nothing.
Three verdicts. ENTER, PASS (the evidence was adequate and
argued against it), and ABSTAIN (the evidence was not adequate to argue either
way). Collapsing the last two would let the agent claim a market view it never
had.
Missing data shrinks the position, mechanically. No rule says "if safety is
unavailable then halve". A gap lowers completeness, completeness lowers evidence
quality, and quality sets size. The four quality components combine as a
weighted geometric mean, so a component at zero takes the whole score to zero.
Enthusiasm can't average away an absent safety check.
Agreement is checked for independence. Three accounts quoting one thesis is
one observation in disguise, and counting it as three is the easiest way for a
multi-KOL agent to be confidently wrong. Every echo is recorded with the post it
repeats and the reason it was demoted.
Every decision pre-registers its own falsification. Falsifiers are written
before the outcome is known, evaluate mechanically, and are hashed into the
decision_id. Move a threshold, a size, or a review date after the fact and you
get a different id, so any edit shows as a new decision. Refusals commit too: a
PASS records what would flip it.
Seeing it
Three authors mentioned the token; two of them were independent. Swap in the
market fixture with no safety score and the same evidence sizes at 3.08%, down from 3.28%. Nothing in the rules changed, only what was known.
hanko audit re-derives every recorded decision from stored bytes and insists it
reproduces. That is the CI gate that turns "preserves a repeatable reasoning
trail" from a README claim into a failing build.
The review loop
At review_at, each falsifier is evaluated against observed facts and the
decision is graded against what it committed to. Whether it made money is recorded but does not decide the grade.
Those come apart more often than is comfortable. An entry can be profitable and
still falsified: the price rose while the liquidity that justified the size
drained away, meaning the reasoning was wrong and the outcome was luck. Both
facts are recorded and reported separately. Grading on profit alone is how an
agent learns to repeat lucky mistakes.
The third outcome carries the weight. When the metric a falsifier names is
unavailable at review time, the check is UNCHECKABLE, never quietly counted
as passing. A decision that could not be checked is not a decision that was
right. Inconclusive reviews are excluded from every rate and reported as their
own number, because an agent that silently drops what it could not verify is
grading itself on a sample it chose after the fact.
Refusals are graded too. A PASS commits to what would have flipped it, so
an agent that only marks the trades it took never learns what its caution cost.
One review per decision, enforced. Re-reviewing until the answer improves is
the exact failure pre-registration exists to prevent, so the ledger refuses a second review.
The liquidity_usd line says exactly what it checked: one reading, no
intermediate ones. price_usd and liquidity_usd are graded against every
sample hanko sweep --samples logs between the decision and the review
(D-13) --
a breach that recovers before review time still falsifies the thesis it
broke, and a review built from a single end-of-window reading says so.
The scorecard
Said 0.7, was right 50% of the time. The agent is overconfident, and says so.
Per-voice reliability is earned from the agent's own audited history. Follower counts play no part, and echoes earn neither credit nor blame. A voice with no scored decisions gets None. Reporting it as zero would defame
it; reporting it as 100% would promote it. Per-rule reliability answers the
uncomfortable question: a rule that is always satisfied on losers is not a
filter, it is decoration.
The sweep
hanko decide handles one token, from one source, by hand. A real watchlist
needs the same thing done for twenty tokens across several sources on a
schedule, without one dead source or one bad token taking the rest of the
run down with it. hanko/sweep.py is that orchestration.
One pass: decide on every entry in the watchlist, then grade every decision
the ledgers say is due. A failing token is recorded in the report and the
sweep moves on. A token dropped from the watchlist still gets its outstanding decisions reviewed.
Every snapshot in one sweep is stamped at the sweep's own as_of. The several source calls inside one pass take a few
real seconds, but they all belong to the same logical instant, and a sweep
re-run against unchanged sources should reach the same decision_id, even if the retry lands a second later.
Review takes a fresh look. fresh_observations()
re-collects evidence and re-derives independent_voices at review time. The whole point of a
review is what's true now; a stale copy of "true then" would grade a
decision against itself.
--samples samples.jsonl gives the window something to actually check.
Every pass appends one market reading per watched token, reusing the facts
that pass already fetched. A decision that becomes due later is graded against every reading logged since it was made. That lets an ANY_POINT_IN_WINDOW falsifier
(D-13)
catch a price or liquidity breach that recovers before the clock gets there.
Omit it and hanko sweep keeps the single end-of-window reading it always
took; the review says which of the two it used.
Meant to be invoked by something that already knows how to schedule things:
cron, a scheduled GitHub Action, Windows Task Scheduler. Each call does one pass and exits, so a failed run is just a failed invocation.
This repository runs one. .github/workflows/sealed-sweep.yml
seals watchlist.live.json every three hours with no
key at all, then verifies the run from its committed files, upgrades any
pending Bitcoin proofs, and writes live/summary.json for
/ledger:
--interpreter lexicon is the keyword reader taught to recognise "Bitcoin"
as well as "$BTC", matching words whole. --lookback-hours 24 reads only
the last day of a feed, since a Telegram page or an RSS feed keeps days of
history that would otherwise score as stale (D-20).
Sweep tests run offline, using a fixture stand-in for RYO tool responses
(FixtureFactsSource, shaped like the live tool sources) alongside the existing evidence fixtures.
Covered: a failed evidence source or fact tool becoming a recorded gap, one bad token not cancelling the rest of the sweep, a decision
already in the ledger being skipped, a token dropped
from the watchlist still getting reviewed, and review pulling fresh observations.
Architecture, decisions, limitations
ARCHITECTURE.md: the one rule, the pipeline as a
diagram, what each package owns, and how exit_liquidity sizes itself as
the same size gate decide() runs internally.
DECISIONS.md: 21 engineering decisions in the
order they happened, including several real bugs found and fixed during
this build and what each one actually broke.
LIMITATIONS.md: what hasn't been shown yet, stated plainly.
SEALING.md: hanko seal -- a numbered manifest of an
entire watchlist run (errors included), anchored to
public OpenTimestamps calendars, published before the outcome is known.
Next
Wire sweep, audit, and scorecard into CI or a scheduled run, and feed
per-voice reliability back into evidence weighting so the agent's own track
record starts shaping what it believes.
Evidence update, 30 Oct 2026
Hanko 2.0.0 is published in the official MCP registry as io.github.IamHarrie-Labs/hanko.
- Retrospective baseline comparison: 76 eligible reviewed opportunities produce identical entry selections under Hanko and a simple rule. This sample establishes no selection advantage. Reproduce with
python scripts/compare_baseline.py.
- A sealed SOL refusal proved wrong: the original commitment, late review-time observations, and what the extra voice does and does not establish.
The first successful model-read workflow sealed run_547a6eaf6c69d992 on 30 Oct: 21 model calls, no fallbacks, three unnamed-token readings rejected, and all four decisions independently replayed. Its Bitcoin timestamp is confirmed.