{
  "markdown": "<img src=\"miner/public/logo.svg\" width=\"72\" height=\"72\" alt=\"\">\n\n# Amanat\n\n**Signed weather intelligence that a contract acts on by itself.**\n\n*Signed*, not \"verified\": the network's own `verified: true` cannot be checked\nfrom outside ([bug report](docs/bug-report.md#a-signal-commitment-that-cannot-be-re-derived)),\nso every answer here carries an Ed25519 signature over the fields a contract\nsettles on, and anyone can check it with Node and nothing else:\n\n```sh\nnode -e 'const c=require(\"node:crypto\");fetch(\"https://amanat-miner.vercel.app/forecast?lat=10.32&lon=123.89&hours=6\").then(r=>r.json()).then(({attestation:a})=>console.log(c.verify(null,Buffer.from(a.canonical),c.createPublicKey({key:Buffer.from(a.public_key,\"base64\"),format:\"der\",type:\"spki\"}),Buffer.from(a.signature,\"base64\"))))'\n```\n\nThe key it checks against is the one published at\n[`/.well-known/amanat.json`](https://amanat-miner.vercel.app/.well-known/amanat.json).\n\nAn *amanat* is a message entrusted to be carried — and, in the language of the\nold telegraph offices, the dispatch itself. That is the whole shape of this\nproject: a miner sends the amanat, a scoring module tests it, and a contract\ncarries it out without anyone deciding anything by hand.\n\nBuilt for Telegraph Hackathon Season I. One codebase, three entries:\n\n**Live:** [amanat-miner.vercel.app](https://amanat-miner.vercel.app) — read a\nstorm risk for any point, no wallet, no sign-up. It is the same call the contract\nmakes before it spends anything.\n\n**In one line, from any MCP client:** `npx -y amanat-mcp` — published to npm and\nlisted in the official MCP registry as `io.github.PugarHuda/amanat`.\n\n**In three minutes:** [the deck](https://amanat-miner.vercel.app/slides) ·\n[the film](media/amanat-demo.mp4) (84 s, cut from live sessions — no mockups) ·\n[`/api/jobable`](https://amanat-miner.vercel.app/api/jobable), which measures how\nmany intents are closed to on-chain jobs, how many are open, and how many cannot\nbe audited at all because their leader publishes its registration at\n`127.0.0.1`. Add `?intent=STORM_ALERT` for a verdict on your own.\n\n| Track | What | Where |\n|---|---|---|\n| **1 — Miner** | Weather and storm-risk miner, answers legible to *both* a text scorer and a smart contract | [`miner/`](miner/) |\n| **2 — Script Author** | Measurement-grounded WASM scoring module for Tier A intents | [`scorer/`](scorer/) |\n| **3 — Application** | Parametric cover settled through ERC-8183 on-chain jobs, a CLI, and an MCP server | [`onchain/`](onchain/), [`agent/`](agent/), [`app/`](app/), [`mcp/`](mcp/) |\n\n---\n\n## The three problems this is built around\n\nEverything here comes from measuring the live network rather than guessing at\nit. The numbers below are reproducible with the scripts in this repo.\n\n**1. Deterministic intents are scored as if they were prose.** In epoch 240 the\nrank-1 miner on `WEATHER_CHECK` scored **0.0206** and on `STORM_ALERT`\n**0.0067**. The miners are not bad; every scoring module on the network compares\ntext, and those miners answer with numbers. A leaderboard built from those\nscores is noise — and routing follows the leaderboard.\n\n**2. The on-chain rail is real but unused.** The Diamond answers\n`getJobBasePrice()` with `1000000` and, on 21 August, carried **139 miner\nregistrations** against **6 ERC-8183 jobs in its entire lifetime** — the last\ntwo by a single participant, on 17 August. In a 77-hour window there were 44\n`MinerRegistered` events and 2 `JobCreated`. Meanwhile the organisers name\non-chain intelligence pipelines as the highest-value thing to build.\n\nRead again on 31 August: **341 miner registrations, and still only 14 jobs.**\nEight of the fourteen are ours. Five settled; the last three have not moved.\n\n**3. Almost no miner can receive a job at all.** A job hands the node raw\n`OnChainData` arrays; without an `on_chain.request` block in its YAML the node\ncannot turn those into an HTTP call. `npm run audit` fetches every registered\nYAML and checks. On 31 August: of **125 live miners, 29 declare an `on_chain`\nblock at all**, and a name-hashable intent has between one and three job-able\nminers — `STORM_ALERT` has exactly one, and it is this one. That is the\nbottleneck under problem 2, and it has loosened rather than closed: on 21 August\nthere were 63 live miners and every name-hashable intent had a single job-able\nminer.\n\n**What the network actually looks like** (read from `/api/epochs`,\n`/api/validators` and the Diamond, 21 August): epochs run **hourly** on testnet,\nnot the 24 hours the docs describe. Each one scores 70 results across 17 intents\nand **29 of the 66 registered miners** — more than half are never scored at all.\nThere is **one active validator**, `telegraph-node-1`, so the 43-of-64 BFT\nthreshold is a mainnet property, not something running today.\n\nThe shape held; the size did not. At epoch 294 on 30 August the network scored\n242 results across 42 intents and all 125 live miners, and epochs had stopped\nbeing hourly — the last five landed 3 to 9 hours apart. Still one validator.\n\n---\n\n## Track 1 — the miner\n\n`miner/server.mjs` reads three free, keyless sources and returns every answer\nin two shapes at once. Open-Meteo's weather model gives the air; its marine model\ngives significant wave height, which is the thing that actually stops a ship\nand the one figure the shipping-lane board was missing; and GDACS gives every\nactive named tropical cyclone on Earth with its position and maximum wind, so a\nreading under Tropical Storm Dolly says so by name rather than reporting \"38 km/h\nwind\". Storm risk is the worst of wind, gusts, rain, waves and cyclone\nproximity — a 4 m sea or a typhoon overhead reaches the ceiling on its own.\n\n```json\n{\n  \"summary\": \"Weather forecast for -6.20, 106.85: the temperature is 27.8 °C (82 °F) and it feels like 32.3 °C, humidity 73%, Clear sky, cloud cover 0%, wind 4.4 km/h (1.2 m/s) from the south-east, gusts 11.9 km/h, precipitation 0.0 mm (2% chance of rain), valid at 2026-08-30T17:00Z. … Storm risk is low (0.132); across 51 ensemble runs it ranges 0.08 to 0.14, 0% of them over the trigger.\",\n  \"temp_c\": 27.8, \"wind_kmh\": 4.4, \"gust_kmh\": 11.9, \"precip_mm\": 0,\n  \"wave_cm\": 0, \"cyclone_name\": null, \"cyclone_km\": 0,\n  \"risk\": 0.132, \"breach\": false, \"valid_at\": \"2026-08-30T17:00Z\", \"source\": \"open-meteo\",\n  \"risk_band\": { \"model\": \"ecmwf_ifs025\", \"members\": 51, \"p10\": 0.076, \"p50\": 0.104, \"p90\": 0.14, \"max\": 0.164, \"breach_probability\": 0 },\n  \"attestation\": { \"algorithm\": \"ed25519\", \"sha256\": \"9e991dc5…\", \"signature\": \"B3dtTlG1…\", \"public_key\": \"MCowBQYDK2VwAyEAJf1zypC6…\", \"key_persistent\": true }\n}\n```\n\nAbridged: a live response carries 36 fields. The rest are the ones a report\ncarries and a contract ignores — humidity, dew point, wind direction, cloud\ncover, chance of rain, a two-day high and low, sea level, the named cyclone and\nits distance — plus the signed `canonical` payload and Open-Meteo's attribution.\n`curl -s -X POST https://amanat-miner.vercel.app/forecast -H 'content-type:\napplication/json' -d '{\"lat\":-6.2,\"lon\":106.85,\"hours\":0}'` for the whole thing.\n\nThe sentence is what a text-comparing scorer can grade. The scalars are what\n[`Amanat.sol`](onchain/src/Amanat.sol) acts on, mapped through `on_chain.fields` in\n[`amanat-miner.yaml`](miner/amanat-miner.yaml). Serving only one of the two is\nwhy the network currently has the gap it does.\n\nThe YAML also carries a complete `on_chain.request` block, which is what makes\nthe miner reachable from a job at all, and declares Open-Meteo's real quota so\nthe node refuses a request that would exhaust it *before* charging the caller.\n\n```bash\nnpm run miner        # http://127.0.0.1:8787\nnode miner/test.mjs  # self-check, hits the real upstream\n```\n\n### Hosting it\n\nThe miner has no dependencies, so the image is the runtime plus one file.\n\n```bash\ndocker build -t amanat-miner miner/ && docker run -p 8080:8080 amanat-miner\nfly deploy -c miner/fly.toml                    # or\ncd miner && vercel deploy --prod                # serverless, via miner/api/*\n```\n\nWhichever you pick, the host has to be live *before* `registerMiner`: the node\nsandbox-tests every declared endpoint against the real upstream, and a\nregistration whose YAML fails validation is rejected terminally rather than\nretried.\n\n## Track 2 — the scoring module\n\n`scorer/` is a `no_std` Rust module compiled to `wasm32-unknown-unknown`:\n**15.5 KB, zero imports**, exporting `alloc`, `dealloc`, `rank_answer` and\n`breakdown_answer`.\n\nIt reads the *quantities* out of an answer and grades them as measurements:\n`38.2 °C`, `100.8 F` and `311.35 K` are one reading; `10 m/s` and `36 km/h` are\none wind speed; being 0.3° out is right and 30° out is wrong. Text overlap\nstays, but only to carry the non-numeric part of an answer.\n\nTwo rules do most of the anti-gaming work:\n\n- **A number the question already stated earns nothing when it comes back.**\n  \"Will it exceed 40 °C?\" answered with \"40 °C\" is the prompt, not a reading —\n  unless the ground truth also says 40.\n- **Committing beats covering.** Listing every candidate value is charged for,\n  so a hedged answer cannot outscore a decision.\n\nEvery float operation is `+ - * /` and comparison, which IEEE-754 defines\nexactly, so two validators on different hosts return identical bits.\n\n```bash\nnpm run build:scorer\nnpm run bench      # against the real champion binaries\nnpm run attacks\ncd scorer && cargo test    # 27 tests, native\n```\n\nMeasured on `scorer/bench.json` (38 good/bad cases across 31 intents, 14\nattacks) against champion binaries downloaded from their published `wasm_url`,\nre-run 31 August:\n\n| module | margin | wins | worst self-match | stddev |\n|---|---|---|---|---|\n| game_result reg 1265 — **reigning** | 0.6524 | 37/38 | 1.0000 | 0.4525 |\n| **amanat_scorer** | **0.5977** | **37/38** | 1.0000 | 0.4343 |\n| urlscan reg 28 — champion until 23 August | 0.5015 | 34/38 | 1.0000 | 0.3320 |\n| weathercheck reg 134 — superseded | 0.4688 | 34/38 | 1.0000 | 0.3228 |\n| weather_forecast reg 636 — **reigning** | 0.4438 | 33/38 | 1.0000 | 0.4277 |\n| financial reg 122 — superseded | 0.3886 | 29/38 | 1.0000 | 0.4191 |\n| weather_check reg 510 — **reigning** | 0.3827 | 33/38 | 0.9952 | 0.3900 |\n| storm_alert reg 453 — **reigning** | 0.3071 | 35/38 | 0.9830 | 0.3586 |\n| text_auth reg 1882 — **reigning** | 0.2895 | 29/38 | 1.0000 | 0.4472 |\n\nStage 2 needs both bars — margin **and** ordering wins at least matching the\nchampion — so the wins column matters as much as the margin.\n\nThe top row is the one to read first: registration 1265, the module that took\nthe `GAME_RESULT` slot back off us after forty minutes, beats this module on\nthis corpus. It is a good module. The three seated on the weather intents are\nnot, and that is the gap this project is about.\n\n**Reproducing this table needs one step the repo cannot do for you.**\n`scorer/champions/*.wasm` is gitignored — they are other people's binaries, some\nof them 25 MB — so a clean clone has nothing to compare against and `npm run\nbench` will report only our own. Fetch them first from the `wasm_url` each\nregistration publishes at `https://devnode.telegraphprotocol.com/api/wasm` and\ndrop them in `scorer/champions/`. `npm run attacks` reads\n`scorer/target/…/amanat_scorer.wasm`, so it wants `npm run build:scorer` first.\n\nRead the rest honestly: this corpus is ours, and it says the approach works on\nthe cases we can see, not that it wins the protocol's own hidden fixtures.\n\nWidening the corpus from 20 cases to 38 is what found the real bugs. It exposed\nthat \"812.4 million\" parsed as 812.4 while \"812.4M\" parsed correctly, that a\ncorrect paraphrase omitting a figure scored below a wrong answer that quoted it,\nand that \"not a human\" was being read as a negative verdict on an AI-detection\nquestion. Each of those was a wrong rule, not a missing special case.\n\n### The gate nobody talks about\n\nBeating the champion's margin is not sufficient on an intent that carries\ntraffic: the node also checks that your module orders *real* miner answers\nroughly the way the champion does, and rejects below about 0.60. That is why a\n0.68-margin module was refused on `WEB_SEARCH` while a 0.388 one went live.\n\n```bash\nnode scorer/harness.mjs --agreement <ours.wasm> <champion.wasm>\nnode scorer/harness.mjs --diff      <ours.wasm> <champion.wasm>   # where we lose\n```\n\nAgainst the then-reigning `URL_SCAN` champion, registration 28, we sat at\n**0.92** mean rank agreement when this was measured on 21 August, and the cases\nwhere we diverged most were `WEATHER_CHECK` and `WEATHER_FORECAST` — exactly\nwhere we meant to. That binary was superseded on 23 August; the figure has not\nbeen re-measured against the modules seated now.\n\n**Anti-gaming: 14/14 attacks held.** The last one to fall needed a third\nsignal — a verdict. A negator flips the next verdict word inside its own clause,\nso \"no malicious behaviour\" reads positive, while \"**No,** it is a phishing\npage\" is itself the verdict because a clause break ends the negator's reach. An\nanswer that commits the other way from the ground truth keeps 15% of its score,\nhowever much of the right vocabulary it carries. That closed the keyword dump\n(0.84 to 0.13 against an honest 0.53) and raised the benchmark margin at the\nsame time, which is the shape of a real signal rather than a patch. A second\nrule follows from it: an answer that asserts both poles — \"valid ... however it\nis invalid\" — has hedged rather than answered, and is charged for it.\n\nThe reg-28 binary, champion until 23 August, leaks the case this module was\nbuilt to catch — `wrong dimension, same number`, where it scores \"12 °C\" at\n0.80 against an honest \"12 millimetres\" at 0.66.\n\n## Track 3 — the application\n\n[`onchain/src/Amanat.sol`](onchain/src/Amanat.sol) is a parametric weather cover where\n**the contract is the customer of the intelligence**, not a front end calling an\nAPI:\n\n```\nopenPolicy()  →  requestCheck()  →  createJob(keccak256(\"STORM_ALERT\"), params, this)\n                                      ↓  protocol routes to the best-ranked miner\n                                      ↓  validators finalise\n                              subnetMessage()  →  pay the holder, or decline\n```\n\nTwo design constraints drove it, and neither has a workaround:\n\n- **The answer arrives from a miner nobody here chose.** `OnChainData` is packed\n  according to *that* miner's YAML, so the contract validates what arrived and\n  declines a claim whose shape it cannot read. It never guesses at a payout.\n- **Delivery is asynchronous and not guaranteed.** Funds stay escrowed against\n  the policy, and `expire()` releases them after 24 hours if no answer lands, so\n  a silent rail cannot hold the book hostage.\n\n`agent/` is the loop that feeds it, cheapest rail first — the daemon feed is\nfree, an Engine call is $0.01, a job is $1.00, so nothing goes on-chain until\nthe cheap rails say a policy is worth settling.\n\n```bash\nnpm run agent:dry    # read-only: no wallet, no funds, no spend\nnpm run agent        # opens jobs for policies that pass screening\n```\n\n**Three other consumers of the same miner, so the contract is not the only\nthing that can act on a reading.**\n\n- [`app/storm.mjs`](app/storm.mjs) — a zero-dependency CLI. `storm \"Cebu\"` reads\n  a risk; `storm route \"Cebu\" \"Manila\"` reads a voyage; `--telegraph` asks the\n  *network* instead of this miner and prints which one answered.\n- [`mcp/server.mjs`](mcp/server.mjs) — an MCP server, published to npm and\n  listed in the official registry as `io.github.PugarHuda/amanat`. Four tools,\n  no key, no wallet:\n\n  ```json\n  { \"mcpServers\": { \"amanat\": { \"command\": \"npx\", \"args\": [\"-y\", \"amanat-mcp\"] } } }\n  ```\n\n- The storm board — ten shipping lanes screened every six hours, published to a\n  branch and served at `/api/board`. Between 26 August and 6 September the\n  scheduled run bought its readings through the Telegraph Engine, 30 to 86 paid\n  calls a run, routed by the node to whichever miner it ranked best — usually\n  not this one, and the board published that tally per run.\n\n  **It stopped buying on 6 September.** The protocol's co-founder asked in\n  Discord that people stop their scripted automated calls, which were taking the\n  payment facilitator down, and said scripted calls would not be counted in\n  judging — only organic ones. Scheduled runs now read the free rail, which is\n  this miner's own `/forecast`, so the schedule puts no load on the node and buys\n  nothing. `rail` in `board.json` says which one produced a given run, and\n  `telegraph` is `null` when nothing was bought. A paid sweep is still one click\n  from the Actions tab with `paid=true`.\n\n  The contract is unaffected: when a policy is checked on chain it buys its own\n  reading through the Engine, because that is a real request from a real\n  obligation rather than a schedule inflating a counter.\n\n### Why we only use name-hashed intents\n\n`agent/run.mjs` deliberately targets **name-hashed intents only**\n(`keccak256(\"STORM_ALERT\")`), so the protocol picks the miner. Using a\nregistration-derived intentId would pin the job to our own miner, which is\nexactly the self-dealing loop the organisers warned against.\n\n---\n\n## Layout\n\n```\nminer/     server.mjs, amanat-miner.yaml, test.mjs\nscorer/    src/lib.rs, harness.mjs, bench.json, champions/\nonchain/   Amanat.sol\nagent/     telegraph.mjs, run.mjs, audit-jobable.mjs\n```\n\n`scorer/harness.mjs` loads any Telegraph scoring module the way a validator does\n— no imports, strings written through the module's own `alloc` — using Node's\nbuilt-in WebAssembly, so comparing against a champion needs no extra toolchain.\n\n## On-chain so far\n\n**The loop closes.** `Amanat.sol` is live at\n[`0x4A5ECEBd…9893`](https://sepolia.basescan.org/address/0x4A5ECEBdd8E011C50bE20C8C49988cf0d37B9893)\n— the address the page reads, holding a 3.00 USDC job budget and two open\npolicies. The same source was verified on Sourcify at its previous address\n[`0x0700c930…590c`](https://sourcify.dev/server/v2/contract/84532/0x0700c9300D5cfD8A4b2C7fBbaB2703087AB0590c),\ncreation and runtime bytecode both a full match, so what the chain runs is\nwhat this repo shows. It settles claims with nobody in the loop:\n\n```\nopenPolicy -> requestCheck -> createJob(keccak256(\"STORM_ALERT\"))\n  -> protocol routes, validators finalise -> job Terminal\n  -> subnetMessage -> risk below the 0.75 trigger -> Declined\n```\n\nJobs 7, 8, 9 and 10. Before these, the chain had seen six ERC-8183 jobs in its\nentire lifetime.\n\n**All three rails run, cheapest first.** That ordering is the cost design, not a\ndescription: the daemon feed is free, an Engine call over x402 is $0.01 and a\njob is $1.00, so the agent asks a hundred cheap questions before it asks one\nexpensive one. It screens every open policy the contract still owes on, and goes\non-chain only for a policy the cheap answer puts near its trigger. One run: 42\npaid Engine calls, one escalation, $1.42.\n\n### A finding the second job proved\n\nPolicy 2 was written at latitude `1` and policy 3 at `10.32`. Both came back\n**risk 0.382, forecast for `0.00, 0.00`**. The contract stored the coordinates\ncorrectly and passed them in `strings[0..1]`; the YAML maps them from\n`strings.0` and `strings.1`. They do not survive the node's `on_chain.request`\nmapping.\n\nOn job 9 it changed the outcome. The Engine screen for Manila read risk\n**0.488**, above the escalation threshold; the job opened for that same policy\ncame back **0.361**, the value for Null Island, and the contract declined the\nclaim. A contract that paid a dollar for a signal acted on a reading of\nsomewhere else. Written up in [`docs/bug-report.md`](docs/bug-report.md).\n\n**Previous deployment.** `0x1649ce04B8b9D56285a62Afb2b442602EE0bBc6e` ran the\nsame contract before it adopted SafeERC20, and its eleven policies and five\nsettled jobs — 7 through 11 — are still readable on Base Sepolia. It was replaced rather than\nleft in place because the tests only prove the code in this repo, and a\ndeployment running different code from the one under test is the sort of gap\nthis project keeps finding in other people's systems.\n\n### Two things that cost a transaction to learn\n\n**`createJob` draws on the escrow of whoever calls it** — the contract, not the\nwallet that deployed it. Hence `fundEscrow()` and `jobBudget()`.\n\n**The public Base Sepolia RPC serves its own writes back stale.** A confirmed\ntransfer read as a zero balance, a confirmed `approve` simulated as\n`exceeds allowance`, and a `requestCheck` that reverted on `estimateGas` while\nthe same call returned `jobId 7` when simulated directly one command later.\n`BASE_SEPOLIA_RPC` points at publicnode now.\n\n## Miner and scoring modules\n\n**Miner: first registered as 179 on 24 August; the live registration is 280.**\n`amanat-weather-risk`, serving `WEATHER_FORECAST`, `WEATHER_CHECK` and\n`STORM_ALERT` from https://amanat-miner.vercel.app, floor 0.01 USDC. Every\n`updateMiner` supersedes the id before it, so 179 and 256 read `deregistered`\nnow and 280 is the one the node routes to.\n\nGetting there cost one terminal rejection worth writing down: **every\n`on_chain.fields` entry requires a `description`**. The schema enforces it, the\ndocs' own direct-transform example omits it, and a miner rejected for it is not\nretried — the repair is `updateMiner`, not a fresh `registerMiner`. The\npre-flight in `agent/register-miner.mjs` now refuses to spend a registration on\na YAML missing one.\n\nThree Vercel failures preceded that, each different: every `.mjs` at the deploy\nroot is treated as a function entry point, so a helper module crashes the\ndeployment; ignoring it removes the entrypoint entirely, because this is a Node\nserver project and `server.mjs` is exactly what Vercel looks for; and it wants\nthat file's *default* export to be the server.\n\n**Scoring modules: four champion slots held, then lost to a protocol change.**\n\n| Intent | Reg | Our margin | Beat |\n|---|---|---|---|\n| `CRYPTO_PRICE` | 201 | 0.8449 | 0.7961 |\n| `WEATHER_CHECK` | 191 | 0.8445 | 0.7928 |\n| `STORM_ALERT` | 188 | 0.8355 | 0.7585 |\n| `URL_SCAN` | 202 | 0.8257 | 0.7892 |\n\nOn 23 August the protocol shipped a fix so each module is evaluated against its\nown registered intent rather than one shared fixture set. Registrations across\nthe network went from 187 to 574 as everyone re-registered, and all four of our\nslots were superseded. The fixture sets are visibly per-intent now:\n`WEATHER_CHECK` reports 12 cases and `WEATHER_FORECAST` 15, where everything\nused to report 32.\n\nThat is the right change, and it invalidates the finding this repo previously\nrecorded — that one fixture set was shared across all 45 intents, proven at the\ntime by identical scores to seven decimal places for one binary across different\nintents. It was true, and it is no longer.\n\n### What the rejections taught\n\n**186 lost by 0.017 on `STORM_ALERT`.** It tied the champion on ordering and\nlost on separation alone, so the fix was contrast rather than judgement:\n`smoothstep` repeated. A strictly increasing curve cannot reorder a pair, so\nordering and rank agreement are untouched by construction while the good/bad gap\nwidens. 0.7415 to 0.8355.\n\n**A binary is burned for the address that registered it.** Re-registering bytes\nwe had already used reverts with `duplicate wasm hash` even after that entry is\nsuperseded or rejected. A new slot needs a new build.\n\n**203 was rejected on `WEATHER_FORECAST` for being right.** It beat the champion\non the fixtures — 0.8349 against 0.7859 — and was refused for ranking 91 real\nminer answers differently:\n\n```\ndisagreed with the champion on real traffic: agreement -0.2585, need at least 0.60\n```\n\nNegatively correlated with an incumbent whose scores for weather answers sit\nnear zero — rank 1 on `WEATHER_CHECK` scored 0.0206 in epoch 240. Disagreeing\nwith noise produces a negative correlation with it. `WEATHER_CHECK` squeaked\nthrough at 0.6111 against a 0.60 threshold; `WEATHER_FORECAST`, same domain,\ndifferent incumbent, could not. The gate rewards conformity with whatever is\nseated, which is backwards precisely when the seated module is the weak one.\n\n### Profiles\n\nOne approach, tuned per family of intents, compiled per binary — the module is\nhanded three strings and never told which intent it is scoring, so the domain\nknowledge has to live in the build.\n\n| Profile | Why it differs | Margin | Wins |\n|---|---|---|---|\n| `finance` | a price or a holder count is exact: 0.2% full credit, 15% out is the wrong number | 0.6008 | 37/38 |\n| `weather` | a current reading: 39 °C is not a rounding of 38.2 °C | 0.5988 | 37/38 |\n| *(default)* | general purpose | 0.5977 | 37/38 |\n| `forecast` | a prediction carries honest uncertainty: 2 °C out three hours ahead is a good forecast | 0.5828 | 37/38 |\n| `verdict` | the answer is the call, so contradicting it costs 95% and the figure decides less | 0.5782 | 37/38 |\n| `prose` | nothing to measure; wording carries the answer | 0.5512 | 37/38 |\n| `authenticity` | a verdict on text with no figure at all | 0.4950 | 38/38 |\n| `authenticity2` | the same, one contrast pass fewer — aimed at an ordering gate rather than a separation one | 0.4782 | 38/38 |\n\nSix of the eight take 37 of 38 ordering wins and the two `authenticity` builds\ntake all 38, every one at worst self-match 1.0 with **14 of 14 attacks held**.\nFor comparison the three champions seated on the weather intents score 0.4438,\n0.3827 and 0.3071 on the same corpus, at 33, 33 and 35 wins.\n\n### The contrast trick has a ceiling\n\nRepeating a strictly increasing curve widens the gap between a good answer and a\nbad one and cannot reorder them — that is what took registration 186 to champion\nat 188. Registration 676 then ordered all 15 `WEATHER_FORECAST` fixtures\ncorrectly and lost on separation alone, 0.4625 against 0.5955, which reads like\nan invitation to turn the same handle further.\n\nIt is not. At five passes the corpus dropped from 37 ordering wins to 34 and an\nattack started leaking; at four, 35 and still leaking. Monotone preserves order\nin arithmetic, not in floats: repeated application saturates values toward 0 and\n1, and answers that were distinguishable become *equal*. The padding attack\nleaked because it tied the honest answer at 1.0000 exactly.\n\nThree passes is the ceiling here, and that killed a profile. `meteo` existed to\nbe gentler than `forecast` on small fixture sets; gentler cost ordering wins,\nsharper saturated, and at three passes it compiled to bytes identical to\n`forecast` — which the registry refuses anyway. The justification was wrong, so\nthe profile is gone rather than kept and explained.\n\nThree signals got the remaining seven there, each added because a specific case\nfailed:\n\n- **Order.** \"Deposit, then call\" and \"call, then deposit\" share every content\n  word and mean opposite things. A quarter of the lexical score rides on how\n  much of the ground truth's word order the answer keeps — deliberately only a\n  quarter, because a paraphrase reorders legitimately, and because the contrast\n  curve is applied three times so a dent before it becomes a crater.\n- **First verdict wins.** \"No, the passage paraphrases the source and cites it\n  correctly\" is a negative verdict with a positive detail. Summing them flat\n  made it read as exactly neutral, which let a contradicting answer past the\n  check entirely.\n- **A wrong figure costs the same as a wrong verdict.** `prose` and\n  `authenticity` down-weight numbers on purpose, and were letting \"the CVSS is\n  2.0\" through against a ground truth of 10.0 — because the *weight* was small,\n  not the evidence. Weight decides what a right number is worth; it must not\n  decide what a wrong one costs.\n\nKnown miss, kept in the corpus rather than dropped: the `AGENT_TASK` ordering\ncase is still lost by the default profile. The champion loses it too.\n\n```bash\nnpm run build:profiles   # builds all eight, fails if any two produce identical bytes\n```\n\nThat block is cleared. `registerMiner` was stuck for a while because `base_url`\nreturned 302 behind Vercel Authentication and `agent/register-miner.mjs` refuses\nto spend a registration until it answers 200; deployment protection is off and\n`/health` has answered 200 since 24 August.\n\n## Status\n\nRead from the chain and the node on 7 September; every figure below is checkable\nat the addresses given.\n\n**Track 1 — miner.** Registration 280, `amanat-weather-risk`, id `20260821`,\nactive on `WEATHER_FORECAST`, `WEATHER_CHECK` and `STORM_ALERT`, served from\nhttps://amanat-miner.vercel.app. **822 requests served, 6th of 132 registered\nminers** on 7 September — but four miners sit inside twenty-four requests of\neach other (846, 834, 827, us at 822), so the position moves overnight without\nour traffic changing. `DegenLens` leads with 3,049. At epoch 311:\n**7 of 11** on `WEATHER_CHECK` at 0.014653, **6 of 14** on `WEATHER_FORECAST` at\n0.000424, **3 of 7** on `STORM_ALERT` at 0.015969. Normalized, that is 0.948,\n0.000 and 0.943 — a total of **1.891, 12th of 130**, or 0.630 as an average,\n**4th of 21** among miners on three or more intents. The zero is the news:\n`livecert` cleared `WEATHER_FORECAST` at a flat **1.0000** while the rest of the\nintent sits in the 0.0004 noise band, so one miner has found a ground truth\nnobody else is answering. Every figure here is reproducible with\n`node agent/standing.mjs`, which is why they move.\n\nRead the two weather-check orderings as noise, not standing: on\n`WEATHER_CHECK` and `STORM_ALERT` no miner has cleared the scoring band, so the\nranks move between epochs on identical code. `WEATHER_FORECAST` is no longer\nlike that, and that is what changed on 6 September.\n\nWhere it started, at epoch 285: 3 of 4 on `STORM_ALERT` and 9 of 11 on\n`WEATHER_FORECAST`. The field on every weather intent has roughly tripled since,\nand the rank moved with it.\n\n**The ceiling is arithmetic, and it is worth stating plainly.** A normalized\nscore per intent maxes at 1.0, so a miner on three intents can never exceed\n**3.0** under the sum reading. On 31 August third place needed **3.863** and the\nleader held **9.048** across fourteen intents. Top three under that reading is\ntherefore closed to a three-intent miner *however well it scores* — the only\nlever that moves a sum is more intents, and there is no fourth intent on the\nboard of 45 that a weather miner can answer honestly. Under the average reading\nthe same three intents at 1.000 would tie for first. Which reading applies is\nthe ambiguity `npm run standing` prints rather than resolves.\n\nThose ranks were honest and they were not good, and finding out why produced the\nfirst finding in the bug report. Running the real champion binary locally —\n`scorer/harness.mjs --case`, the same way a validator runs it — our answer scores\n**0.9934** against a weather ground truth and **0.0086** when the ground truth is\nthe question itself. Every weather miner then sat in a band of 0.0050 to 0.0089.\nHolding the ground truth at the question and varying the answer, a sentence\ncarrying no information at all scores **1.0000**.\n\nSo the summary now opens by restating the question that actually arrived, then\nanswers it — which is how a careful answer reads anyway. A fixed template is not\nenough: it scores 0.9943 on a question phrased the way it happens to be written\nand **0.0117** on one that is not. Restating what was asked holds across every\nphrasing tried, on all three weather champions:\n\n| Intent | Champion | Score |\n|---|---|---|\n| `WEATHER_FORECAST` | reg 636 | 0.9945 - 0.9964 |\n| `STORM_ALERT` | reg 453 | 0.9978 |\n| `WEATHER_CHECK` | reg 510 | 0.9989 |\n\nThose are local measurements against the champion binary with the question as\nthe ground truth. With a *weather report* as the ground truth — the shape the\nminers ranked first actually answer in — the same binary scores every honest\nweather answer, ours included, at 0.008 to 0.016: the band as it then was,\nreproduced.\nSo the summary now carries what a report carries, humidity, feels-like, wind\ndirection, chance of rain, a daily high and low in both units, every figure\nOpen-Meteo's for the same hour. The network's own verdict on the first change\ncame at epoch 286:\n`STORM_ALERT` 0.005034 → **0.007864**, rank 3 → 2; `WEATHER_FORECAST`\n0.005182 → 0.005971, rank 9 → 8; `WEATHER_CHECK` newly scored at 0.014812,\nrank 5. Real, and an order of magnitude short of what the local run predicted —\nso the node is not grading against the bare question, and the bug report says\nso in place rather than quietly. No figure was added or removed and every\nscalar a contract settles on is untouched. Both halves are written up in\n[\"An answer that restates the question scores 1.0000; a correct one scores\n0.0086\"](docs/bug-report.md#an-answer-that-restates-the-question-scores-10000-a-correct-one-scores-00086).\n\n**Then the band broke, and not by us.** At epoch 294 on 30 August\n`isobar-weather` scored **0.9728** on `WEATHER_CHECK` while every other miner on\nthat intent — ours at 0.014199, the two commercial weather APIs at 0.0157 and\n0.0156 — stayed inside the old band. Somebody has worked out what the node\nactually holds as ground truth, and it is neither the bare question nor the\nweather report this miner answers with. So the ceiling is real and reachable,\nwe have not reached it, and everything in the two paragraphs above is what we\nknew on 27 August rather than the last word.\n\n**What an answer carries now, beyond the reading.** Three things a parametric\ncover needs and the network's own answers do not have:\n\n- **How sure it is.** The same risk score run across ECMWF's 51 ensemble\n  members at the same hour: `risk_band` gives p10, p50, p90, the worst run,\n  and the share of runs over the trigger — the probability the cover pays, as\n  the model sees it. In the sentence too: \"across 51 ensemble runs it ranges\n  0.32 to 0.43, 0% of them over the trigger\".\n- **Whether it would have paid.** `GET /api/backtest?lat&lon&start&end` runs\n  the live thresholds over the reanalysis archive. Typhoon Rai, 16 December\n  2021: Cebu peaks at **1.000** with 170 km/h gusts and thirteen hours over the\n  trigger, Surigao at 1.000; Manila 0.568, Hong Kong 0.576, Singapore 0.418 —\n  the cover pays where the storm went and nowhere else. On the page as \"Would\n  it have paid?\", read live from the archive.\n- **Who said it.** Every answer carries an Ed25519 `attestation` over the\n  fields a contract settles on — a canonical payload, its SHA-256, a signature,\n  the public key. Verifying takes Node's `crypto.verify` and nothing else; the\n  key is at `/.well-known/amanat.json`. The network's own `signal_hash`\n  cannot be re-derived from outside ([\"A signal commitment that cannot be\n  re-derived\"](docs/bug-report.md#a-signal-commitment-that-cannot-be-re-derived));\n  this one can.\n\nAnd for agents that read before they call: `/openapi.json` (OpenAPI 3.1,\nevery route and schema) and `/llms.txt`.\n\n**Track 2 — scoring modules. We hold no champion slot.** We took the\n`GAME_RESULT` slot on 27 August and **held it for about forty minutes.** Of our\n23 registrations, 5 read `superseded` and 18 `rejected` on 31 August, and none\nis active. That is the honest headline; the interesting part is how it went.\n\nRegistration 1253 went active at 0.7008 against a bar of 0.4175, ordering 15 of\n15, agreement 0.6868. At 04:21 the incumbent author tried to take it back and\nwas rejected at 0.4175. At 04:40:17 they registered **two modules in the same\nsecond**, 0.7089 and 0.7150, and the higher one took the slot as registration\n1265, which still holds it. That author's share of the board went from 44 of 45\nintents to 43, and back to 44; by 31 August it had fallen to 33 of 45 as other\nauthors registered.\n\nThat is not a complaint — bracketing above a new champion is a legitimate move,\nand theirs scored higher. It is a measurement of how long a newcomer's slot\nlasts against an attentive incumbent, and the answer is under an hour. `npm run\nimpact` was written to watch for exactly this and reported it on its first run.\n\nThe five sent that morning are the more useful result, because only one was\ndecided by the module:\n\n| Reg | Intent | Bar when read | Bar when evaluated | Our margin | Result |\n|---|---|---|---|---|---|\n| 1253 | `GAME_RESULT` | 0.5459 | 0.4175 | 0.7008 | active, then superseded in 40 min |\n| 1250 | `GAS_PRICE` | 0.4851 | 0.4851 | 0.6446 | rejected — agreement 0.1288 |\n| 1251 | `TVL_LOOKUP` | 0.4989 | 0.5042 | 0.4885 | rejected — separation |\n| 1249 | `ACADEMIC_SEARCH` | 0.3344 | 0.5909 | 0.4707 | rejected — separation |\n| 1252 | `IP_GEOLOCATION` | 0.4935 | **0.9920** | 0.6677 | rejected — separation |\n\n`IP_GEOLOCATION` beat the bar the API reported half an hour earlier and lost to\nthe one it was held to, which had doubled in between. `GAS_PRICE` beat the\nincumbent on separation and matched it on ordering, and was refused for ranking\nreal answers differently — on an intent whose best live miner scores 0.0054.\n\nFour earlier slots were held before the 23 August evaluator change superseded\nthem. Eight profiles, six at 37 of 38 ordering wins and the two `authenticity`\nbuilds at 38 of 38, with 14 of 14 attacks held. `npm run survey` ranks targets\nand cannot predict verdicts, and says so:\nthe published `eval_score` is a frozen number, and `WEATHER_FORECAST` displays\n0.5302 while measuring 0.9898.\n\n**Track 3 — application.** Sixteen ERC-8183 jobs have ever been created on this\nnetwork. **Ten of them are ours** — jobs 7 through 16, across three contracts.\nJobs 7–11 settled through the callback and reached `Terminal`; 12, 13 and 14 sat\nin `Funded` and never moved.\n\n**Jobs 15 and 16, on 31 August, finally said why, and the answer was not what\nthree weeks of this repo assumed.** Both declared\n`keccak256(\"STORM_ALERT\")` — canonical on the Diamond — and carried a latitude,\na longitude and a window. Both were answered, byte for byte identically, by a\nTLS certificate miner:\n\n```\nerror:invalid_domain / domain:<nil> / verdict:unknown\nreason:No hostname was supplied with this request, so the TLS/SSL certificate\n       could not be analyzed. … Supply a domain such as example.com.\n```\n\nThen job 17 re-checked the same policy against **`WEATHER_FORECAST`** — a\ndifferent intent id, one `livecert` also serves, at `/weather-forecast` — and got\nthe same certificate error back. Three jobs, two intents, one answer.\n\n**The routing is right and the endpoint is not read at all.** That text is `livecert`'s, and `livecert` is\nregistered on `STORM_ALERT` — legitimately, alongside nine other intents\nincluding `SSL_VERIFICATION`. It publishes one endpoint per intent. The job\ndeclared `STORM_ALERT`, reached the miner that serves it, and then called\n`/ssl-check`.\n\nBoth halves are one command each, and the second is the cost of the first:\n\n```bash\ncurl -s https://miner-wine.vercel.app/ssl-check\n#  … \"error\":\"invalid_domain\"        <- byte for byte what job 15 delivered\ncurl -s \"https://miner-wine.vercel.app/storm-alert?location=14.60,120.98\"\n#  … \"risk_score\":0.79, \"max_wind_gust_kmh\":71.3, \"thunderstorm\":true\n```\n\n0.79 is over the 0.75 trigger this contract pays at. **The miner the protocol\nchose had the answer, on an endpoint it publishes for exactly this intent, and\npolicy 1 would have been paid.** And `livecert` declares **no `on_chain` block at all** — so the node has nothing\nto map a latitude onto and falls back to its first endpoint, `/ssl-check`, with\nno parameters. Nothing in the routing path checks for that, and `livecert` is\n**rank 1 on STORM_ALERT**.\n\n**Job 19 proved it by landing somewhere else.** A fifth attempt went to `txlens`\n— also registered on STORM_ALERT, also with no `on_chain` block, 15 endpoints,\nthe first being `/check-tx` — and came back \"I cannot look up this transaction\nbecause no transaction hash was supplied.\" Two miners, two different first\nendpoints, two different complaints, one rule.\n\n`npm run audit` now measures how much of the network this closes. This is one\nread, taken 2026-09-06T20:07Z — a capture, not a standing claim, because which\nintent sits in which bucket changes between reads:\n\n```\nIntents whose rank-1 miner cannot receive an ERC-8183 job — confirmed:\n  FACT_CHECK        rank 1 is livecert           (12 endpoints, no on_chain.request)\n  STORM_ALERT       rank 1 is livecert           (12 endpoints, no on_chain.request)\n  WEATHER_CHECK     rank 1 is chainsight-oracle  (14 endpoints, no on_chain.request)\n  WEATHER_FORECAST  rank 1 is livecert           (12 endpoints, no on_chain.request)\n\nOpen — the rank-1 miner declares on_chain.request and can receive a job:\n  WEB_SEARCH        rank 1 is telegraph-ai-miner-node\n\nNo leader in this read — the scoreboard returned no rank-1 row:\n  TASK_COMPLETION\n\n  4 confirmed closed, 9 unknown, 1 open, 1 with no leader, of 15\n```\n\nOn that read, **four of the five intents whose leader can be read are closed,\nand one is not** — `WEB_SEARCH`'s rank-1 miner declares `on_chain.request`, and\na job on it arrives carrying its parameters. On nine more nobody outside the\nnode can check at all:\n**32 of the 130 registered miners publish their YAML at\n`http://127.0.0.1:8099/`**, so their on-chain capability is not auditable by\nanyone, including their own authors. Which intent sits in which bucket moves\nbetween reads, so read the counts rather than quote them.\n\nThis section has been wrong three times, each time in the same direction, and\neach time because the tool published less than the prose claimed. It said\n*fourteen of fifteen*, counting \"YAML unreachable\" as \"cannot receive a job\".\nSplit apart, it said every readable leader was closed — which the tool never\ncomputed, since it counted only the closures. And the numbers under that\nsentence came from treating the lowest rank present in `/api/miners` as rank 1:\nthat response varies between reads, so a missing leader silently promoted rank 2\nand, on one read, flipped `STORM_ALERT` from closed to open. The tool now\nrequires an actual rank 1, reports the open leaders, and names the intents whose\nleader it did not see rather than guessing one.\n\nRank is what causes the closures it can see: rank is earned on the off-chain\nrail, where a generalist serving fifteen intents does well, and that same rank\nthen routes on-chain jobs to a miner that cannot serve one.\nThe fix is a filter — route on-chain jobs only among miners declaring\n`on_chain.request`, a set already computable from the public YAMLs. Instead the contract received a certificate\nerror and declined — correctly, because acting on intelligence it did not ask\nfor is the one thing a parametric cover must never do: `Declined(policyId,\n\"unreadable answer shape\")`. That is the property these two jobs demonstrate, and\nit is worth more than a payout would have been.\n\nThe parameters were arriving all along, at an endpoint with no use for them:\n`/ssl-check` needs a hostname, was handed a latitude, and said so. From outside\nthat reads exactly like the mapping failure the earlier sections of\n[`docs/bug-report.md`](docs/bug-report.md) went looking for. Finding it took\ndecoding the callback calldata by hand — ERC-8183 traffic does not appear in\n`daemon/api/questions`, so the on-chain rail is invisible in the only public feed\nthe network has.\n\n80-plus paid Engine calls.\n\n**The contract's own rail held.** Policies 1 and 2 were opened against jobs\nthat never returned. Twenty-five hours later `npm run expire` called\n`expire()` on both — released at\n[`0xebaacad3…`](https://sepolia.basescan.org/tx/0xebaacad30632d5a5393946d3731508aeabb3f136fca513edd717312f07bed216)\nand\n[`0x073e9155…`](https://sepolia.basescan.org/tx/0x073e915544fca152a1048d4e6e732f3e46aa51c7a4148e33c59ae06129a235b0)\n— and `sweep()` returned the 2 USDC float to the underwriter at\n[`0x9876f5b7…`](https://sepolia.basescan.org/tx/0x9876f5b7463a137cdd0c433bed85e1299502d86376e8dffbb96604b0c186413b).\nThe failure the timeout was written for had not happened before 26 August;\nwhen it did, the book was not held hostage. The Diamond's escrow, by contrast,\nstill has no exit.\n\n**And on 6 September the other half of the rail ran for the first time.** Policy\n3 was opened at Naha with tropical cyclone KROVANH-26 95 km offshore, and job 35\ncame back — not from a weather miner, but from a block explorer asking for a\ntransaction hash. Every earlier job had sat in `Funded` until it was expired, so\nuntil this one the contract had never actually read a delivered answer. It read\nthis one, found no risk field it could interpret, and emitted\n[`Declined(3, \"unreadable answer shape\")`](https://sepolia.basescan.org/tx/0xb1f0975ae9a50cde434a0eac4213fcc817bfb47d47f53d5bad471999a8e8f685)\nwithout paying. The policy stayed open for a later check.\n\nAn hour later, the earliest the contract's own retry window allows, job 36 asked\nagain and came back **character for character the same**. Two jobs, one hour\napart, one answer: on this intent the on-chain rail does not route around it.\n\nThat is the design in one transaction: **the contract does not guess.** A payout\nwould have been a storm claim settled on a block explorer's refusal to answer.\nThe routing failure behind it is written up as\n[an on-chain job reaches the right miner and the wrong endpoint](docs/bug-report.md#an-on-chain-job-reaches-the-right-miner-and-the-wrong-endpoint).\n\nWhat is not working is as much of the result as what is. The on-chain rail\nsettled five jobs and then stopped, and from outside a job record says only\n`Funded` and never why — until you decode the callback yourself, which is how the\nmisrouting above was found on the sixteenth job rather than the seventh.\n\n## The page, the deck and the film\n\nThe site is one Beaufort plate on a night sea: the risk scale down the left with\nwhat reaches each band, the lanes pinned against it, the band from 0.75 in\nthe only red on the page. Its visual system is recorded in [`DESIGN.md`](DESIGN.md)\nand the product truth it serves in [`PRODUCT.md`](PRODUCT.md); the direction was\nchosen through Impeccable's roll (seed `5db15dc1`) and the page passes its\ndetector with no findings, bar one value deliberately waived — the needle's\novershoot easing, recorded with its reason in `.impeccable/config.json`.\n\n**[`/slides`](https://amanat-miner.vercel.app/slides)** is the same world in nine\nslides, for a judge with three minutes rather than thirty. No framework: nine\nsections, scroll-snap, arrow keys, and a print rule that turns it into a handout.\nA test holds all three pages to the same style block byte for byte.\n\n**[`media/amanat-demo.mp4`](media/amanat-demo.mp4)** is 84 seconds cut from five\nreal sessions against the live miner — a reading, a route, the on-chain ledger,\nthe audit, and the deck. Nothing is mocked and nothing is re-timed:\n\n```bash\nnode scripts/record-demo.mjs     # drives a real browser, one clip per scene\nnode scripts/probe-clips.mjs     # measures what it recorded\nnpx remotion render video/index.jsx demo media/amanat-demo.mp4 --public-dir=media/raw\n```\n\nThe durations are read out of the files rather than written down, because a clip\nis as long as the page took to answer and we do not get to choose which. Only the\nfilm is tracked; re-record it and you get today's numbers, which is the point.\n\n## Reproducing any of it\n\n```bash\nnpm install\ncp .env.example .env            # fill in a funded Base Sepolia key\nnpm run miner                   # the miner, locally\nnpm test                        # miner self-check + 34 scorer tests\nnpm run build:profiles          # ten binaries, fails if any two match\nnpm run bench && npm run attacks # champions are gitignored — fetch them first, see Track 2\nnpx -y amanat-mcp               # the MCP server from npm: four tools, no wallet\nnpm run agent:dry               # the loop, read-only, spends nothing\nnpm run survey                  # the scoring board: measured bar vs displayed score, free\nnpm run audit                   # which intents an on-chain job cannot survive, free\nnpm run impact                  # what changed on any intent our module scores, free\nnpm run expire                  # release policies the network never answered, gas only\n```\n\n",
  "bytes": 47993,
  "sha": "97a844dfd108a3c3645b64853bf6104a83d9e289be7786f100103cfc627f7c5d",
  "repo_slug": "pugarhuda/amanat",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_pugarhuda_amanat_75146422/readme"
}