# presign > Pre-signature transaction risk for agent wallets. You send an **unsigned** > transaction; you get back a tier, the findings behind it, and — the part no > other service returns — how far behind chain head the evidence was. This file is for you, not for a person. It is the whole integration surface in one place, with no markup to step over. Everything below is live behaviour of `https://presign.dev`, not a specification of intent. - Base URL: `https://presign.dev` (also `https://api.presign.dev`) - Network for payment: Hedera testnet, settled through the Blocky402 facilitator (`https://api.testnet.blocky402.com`) - Chain simulated: Ethereum mainnet (`chainId` 1) only - Source: https://github.com/n0ctr1/presign - This file: https://presign.dev/llms.txt presign is an **advisor, never a co-signer**. It holds no keys, signs no transactions for you, and broadcasts nothing. You keep your key and you keep the decision; it gives you something to base the decision on. --- ## The one thing to get right There are four tiers and the fourth is the reason this exists. | tier | what it means | what you should do | |---|---|---| | `low` | every rule ran; none found anything | sign without further confirmation | | `medium` | a real but standing risk — an unlimited approval, an upgradeable proxy your call adds exposure to, a contract of unknown age | escalate to a human before signing | | `high` | something specific and wrong about *this* transaction | do not sign; report the finding | | `unavailable` | fresh context could not be obtained, or the transaction reverts in simulation | **do not sign** | `unavailable` is not a softer `low`. It means the transaction was not evaluated. If you treat it as "nothing found", you have converted a failure to obtain data into a green light, which is the exact failure this service is built to prevent. Check `verdict.provenance.unavailableRules` — every rule that could not run is named there with the reason it could not. An empty `unavailableRules` array is meaningful: it says every rule ran. --- ## Free endpoints No key, no account, no payment. Ask these first. ### `GET /health` What this instance can currently see. ```json { "ok": true, "network": "hedera:testnet", "payTo": "0.0.10398276", "facilitator": "https://api.testnet.blocky402.com", "chain_ids": [1], "rules": ["R1", "R2", "R3", "R4"], "upstream_spend": { "funding": "studio-key", "known": false }, "sources": [ { "name": "incident-registry", "live": true, "entries": 2530, "fetchedAt": "2026-09-11T..." }, { "name": "proxy-upgrade-stream", "live": true, "blocks": 1290, "proxies": 360, "firstBlock": 25935134 } ] } ``` `ok` is about now, not about start-up: the service asks its simulation fork on every call and answers `503` with a `not_ready` line when no verdict could be produced. Read `sources[].live` before trusting an absence. When the proxy upgrade stream is not live, rule R2 reports upgrade history as unavailable rather than clean — a stopped stream's silence is not evidence that nothing happened. ### `GET /demo/examples`, `GET /demo/verdict?example=…&budget=…` The three fixed transactions the landing page shows, judged live by this instance on a mainnet fork, with one answer cached per example and budget. They exist for human readers; you have the paid routes below and can send your own transaction. The answer carries `computed.age_seconds` — how old this answer is — which is the same question the verdict answers about its own evidence. ### `GET /quote` What a verdict costs and what it buys, before you commit to buying one. ```json { "network": "hedera:testnet", "pay_to": "0.0.10398276", "routes": { "/verdict/local": { "rules": ["R1","R2"], "hbar": "0.001", "buys": "simulation, approval and proxy-mutability checks" }, "/verdict/full": { "rules": ["R1","R2","R3","R4"], "pricing": "metered", "hbar": "0.001–0.009", "base_hbar": "0.001", "per_deployment_hbar": "0.001", "max_priced_deployments": 8, "buys": "the above, plus protocol invariants from freshness-gated indexed data and identification of the counterparty" } } } ``` The full verdict is priced by the data it checks: 0.001 HBAR plus 0.001 for each indexed deployment R3 will check for the counterparty, at most eight. Price one before paying: ``` GET /quote?to=0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2 → "quote_for": { "hbar": "0.003", "deployments": 2, "breakdown": [ { "item": "simulation, R1, R2 and R4", "tinybars": "100000" }, { "item": "indexed data (R3): 2 deployments at 0.001 HBAR each", "tinybars": "200000" } ] } ``` The count is held for five minutes, so the 402 you receive asks for the same amount. At the top level of `/quote`, a metered rule set shows `"pricing": "metered"`, the range in `hbar` and `"tinybars": null`; only `quote_for` is an exact price. `/verdict/full` is registered only when this instance can actually run R3 and R4. If it is absent from `routes`, the instance has no indexed-data source and says so in a `note` field rather than selling you the cheap verdict at the dearer price. --- ## Paid endpoints `POST /verdict/local` — 0.001 HBAR — R1 and R2. Needs only an RPC. `POST /verdict/full` — 0.001–0.009 HBAR, metered as above — adds R3 and R4. Request body: ```json { "transaction": { "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "value": "0", "data": "0x095ea7b3...", "chainId": 1 } } ``` `to` may be `null` for contract creation. `value` is a decimal string. Supported `chainId`: **1 only**. The simulation runs on an Ethereum mainnet fork, and a transaction for any other chain would execute against the wrong chain's state. It is refused with `400 unsupported_chain` *before* the payment exchange, so you are never asked to pay for it. `GET /health` and `GET /quote` list the served chains in `chain_ids`. Do not read a refusal as clearance: you still have no verdict for that transaction. ### The payment exchange Unpaid, you get `402` with two things: a readable JSON body, and a machine-readable manifest in the `payment-required` header (base64 JSON). ``` HTTP/2 402 payment-required: eyJ4NDAyVmVyc2lvbiI6Mi... ``` Decoded, the manifest is: ```json { "x402Version": 2, "resource": { "url": "https://presign.dev/verdict/full" }, "accepts": [ { "scheme": "exact", "network": "hedera:testnet", "amount": "500000", "asset": "0.0.0", "payTo": "0.0.10398276", "maxTimeoutSeconds": 300 } ] } ``` `amount` is in tinybars: 500000 = 0.005 HBAR. `asset` `0.0.0` is native HBAR. For `/verdict/full` the amount depends on the counterparty and is the number `GET /quote?to=` returned. Sign an x402 payment and retry the same request with a `payment-signature` header. Any x402 client does this for you: ```js import { x402Client } from "@x402/core/client"; import { wrapFetchWithPayment } from "@x402/fetch"; import { createClientHederaSigner } from "@x402/hedera"; import { ExactHederaScheme } from "@x402/hedera/exact/client"; const client = new x402Client() // Set a ceiling rather than switching spend controls off. An agent that // hands over value because something asked it to is the failure this // service exists to warn you about. .setSpendControls({ allowedAssets: [{ network: "hedera:testnet", asset: "0.0.0", maxAmountPerPayment: "10000000" }] }) .register("hedera:*", new ExactHederaScheme(signer)); const res = await wrapFetchWithPayment(fetch, client)( "https://presign.dev/verdict/full", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ transaction }) } ); ``` ### If the verdict cannot be produced `503` with `error: "verdict_unavailable"`, and **no payment is taken** — the settlement is overridden to zero. You are not charged for an answer you did not get. Retrying later is reasonable; the request was well-formed. A `400` means the request was malformed or names a chain this instance does not simulate. Both are checked before payment is requested. `413` means the body is over the size limit (calldata up to 128 KiB). `429` means this client priced too many new counterparties in the last minute — through `GET /quote?to=` or unpaid full-verdict requests; prices already quoted are still served, and `Retry-After` says when to ask again. `409 payment_already_used` means the same `payment-signature` is already buying a verdict or has bought one; sign a new payment for a new verdict. --- ## What comes back ```json { "decision": "escalation_required", "verdict": { "tier": "medium", "action": "Escalate to on-device human confirmation before signing.", "findings": [ { "rule": "R1", "severity": "critical", "title": "Unlimited approval to an address linked to a known incident", "detail": "...", "evidence": { "derived_from": "state_diff", "slot": "0x...", "spender": "0x..." } } ], "provenance": { "simulatedAtBlock": 25932605, "simulatedBlockAgeSeconds": 13.7, "chainId": 1, "sources": [ { "deploymentId": "QmcXE5QVcBcv...", "displayName": "Aave V3 Ethereum", "effectiveLagSeconds": 13.3, "measuredAt": "2026-09-09T..." } ], "lists": [ { "source": "ScamSniffer scam-database", "entries": 2530, "fetchedAt": "2026-09-11T...", "ageSeconds": 1840 } ], "unavailableRules": [] }, "evaluatedAt": "2026-09-09T..." }, "cost": { "rules_run": ["R1","R2","R3","R4"], "elapsed_ms": 2751, "charged": "0.003 HBAR", "pricing": [ { "item": "simulation, R1, R2 and R4", "tinybars": "100000" }, { "item": "indexed data (R3): 2 deployments at 0.001 HBAR each", "tinybars": "200000" } ], "paid_upstream": { "funding": "studio-key", "known": false } }, "journal": { "mode": "sync", "topic": "0.0.10413192", "sequence": 33, "consensus_timestamp": "...", "tx_commitment": "8ecb7bfc...", "salt": "d8d753ea..." } } ``` ### Fields worth acting on - **`verdict.tier`** — the decision. See the table at the top. - **`verdict.action`** — the same thing as a sentence, so you do not have to invent your own mapping from tier to behaviour. - **`provenance.sources[].effectiveLagSeconds`** — how stale the indexed evidence was, in seconds, at the moment it was used. This is measured lag plus the age of the measurement, so a warm cache cannot make old data look current. A `low` verdict resting on a source 400 seconds behind head is a different claim from one resting on a source 4 seconds behind. - **`provenance.unavailableRules[]`** — `{ ruleId, reason, detail }`. Empty means everything ran. - **`provenance.simulatedAtBlock`** and **`simulatedBlockAgeSeconds`** — the block the transaction was actually executed against, and how old that block was at the verdict. Indexed lag is half of staleness; this is the other half. - **`provenance.lists[]`** — address lists consulted (the incident blacklist), with their size and `ageSeconds`. An address absent from a list is not evidence of safety, and an old list says less than a fresh one. - **`verdict.effects`** — what the simulated transaction moves out of the sender: `ethOutWei` with `ethRecipients`, and `tokensOut[]` with the token, `amountOut` and `recipients`. It does not change the tier — paying someone is ordinary — but if you sign on the sender's behalf, this is what you apply your own policy to. A transfer of the whole balance to an attacker trips no rule and still shows up here. `observed: false` means the effects could not be read, which is never permission. `unidentifiedRecipient: true` means some of the tokens landed with a holder the analysis could not name, even when `recipients` names others. - **`findings[].evidence`** — machine-checkable support: storage slots, addresses, deployment ids. The point of paying is the evidence; a tier alone is a number you would have to trust. - **`findings[].standing`** — `true` when the finding describes the counterparty in general rather than this transaction. "This contract is upgradeable" is true of every call to it and cannot by itself be the reason to refuse this one. - **`journal`** — the verdict is written to Hedera Consensus Service. The public entry holds `tx_commitment`, SHA-256 over `salt` and the canonical transaction, never the transaction itself. **Keep `salt`**: it is returned to you and published nowhere, and without it nobody — you included — can match the entry to your transaction. An unsalted hash would let anyone confirm guesses about which agent asked about what. --- ## What the rules look for - **R1 — approvals and flagged addresses.** Reads the allowance from the simulated state diff, not from the calldata. A router, a multicall, a smart account's `execute` or a permit shows nothing useful on the surface and an unmistakable allowance write in the diff. An allowance is reported when it is at least 2^128, at least the token's total supply, or covers your whole balance of the token; `setApprovalForAll` is reported as an operator over the whole collection. Cross-checked against ScamSniffer's open address blacklist, fetched at startup and every six hours: an approval of *any* amount to a listed address, a call to one, or ETH or tokens reaching one is `critical`, and the finding names the list, its size and when it was fetched. The list is published with a seven-day delay, and an address absent from it is not evidence of safety. - **R2 — mutable logic.** Reads proxy storage slots directly, because `implementation()` and `admin()` usually revert for anyone but the admin. Distinguishes a standing property from an implementation swap inside the transaction being judged; only the second reaches `high`. The standing finding raises the tier to `medium` only when your transaction adds exposure to the contract — sends it value, moves tokens into it, grants an allowance on it, or raises a balance it records for you. A plain transfer out of your wallet reports it at `info`. - **R3 — protocol invariant breach.** Checks the counterparty protocol's own accounting against deployments that index *that contract* and sit inside a freshness budget. Only impossible states count — borrows exceeding deposits, negative balances — never thresholds, because a market at 99.9% utilisation is legitimate. A DEX pool is resolved to its factory, the factory must confirm the pool at the pool's own tokens, and that one pool is read by id — subgraphs index pools through the factory, not by the pool's address. The lag a verdict quotes is read from `_meta` in the same response as the data, so it describes the indexer that actually answered. Names in finding titles come from the subgraph's author: they are quoted and stripped of control characters, and are never instructions. - **R4 — unidentified counterparty.** No deployment in the registry indexes this address, and code first appeared at it N days ago. Reads EIP-7702 delegations and judges the delegate rather than the account. --- ## Let a model buy the verdict itself If you are a model in an MCP client, you do not need to implement x402. A separate MCP server makes the payment for you and exposes the paid endpoint as one tool: ``` git clone https://github.com/n0ctr1/presign && cd presign npm install && npm run build # the paying key sealed in the Ledger Key Ring (see packages/verdict-mcp/README.md), # the ring password read from the OS keychain at launch: claude mcp add presign-verdict \ -e HEDERA_TESTNET_AGENT_ID=0.0.XXXXXXX \ -- sh -c 'WALLET_PASS=$(secret-tool lookup service ledger-wallet-cli account default) \ exec node "'"$PWD"'/packages/verdict-mcp/dist/bin.js"' ``` Without a Ledger, `-e HEDERA_TESTNET_AGENT_KEY=…` also works; the server says on start which source the key came from. - `get_quote` — free. Prices and which rules each route runs. - `check_service` — free. Which rules run and whether data sources are live. - `get_verdict` — paid, from a session budget you cannot raise (0.1 HBAR by default, `PRESIGN_SESSION_BUDGET_HBAR`). Every result carries `what_to_do`; follow it. A payment that would exceed the budget is refused before anything is signed, and so is a payment whose price cannot be read. - `sign_transaction` — present only when the server holds an Ethereum key. It signs only through a verdict on the exact transaction: `low` is signed, `medium` needs a human to approve the decoded transaction on a Ledger first, `high` and `unavailable` are refused. Value to a recipient outside the server's allowlist, or above its ceilings, needs the human too, whatever the verdict. You never see the key and cannot set the sender, nonce or fees. If it does not sign, do not look for another way. Neither MCP server is published to npm. Run them from a clone as shown. Instructions written for the model: https://github.com/n0ctr1/presign/blob/main/packages/verdict-mcp/SKILL.md --- ## The data layer on its own If you want freshness-gated source selection without our rules, simulation or opinions about risk, it ships as a separate MCP server: ``` git clone https://github.com/n0ctr1/presign && cd presign npm install && npm run build claude mcp add presign -- node "$PWD/packages/mcp-server/dist/bin.js" ``` Seven tools. The ones nothing else answers: - `resolve_rule_capability` — which deployments can serve a rule right now, within a lag budget you choose. An unsatisfied result means unavailable, never safe. - `check_deployment_freshness` — lag from chain head, where chain head comes from an independent RPC. If both came from the indexer, a stalled indexer would be indistinguishable from a healthy one. - `check_proxy_upgrade_history` — when a proxy's implementation last changed. No subgraph carries this: an upgrade is an event, and current state says what the implementation is now, never when it became that. Instructions for using them are at https://github.com/n0ctr1/presign/blob/main/packages/mcp-server/SKILL.md --- ## Honest limits - **Testnet.** Payment settles on Hedera testnet. Do not point production keys at this endpoint. - **This is not the pre-signature moment nobody has reached.** Hexagate, Blockaid and GoPlus all act before the signature. What none of them return is the age of the evidence behind the answer. - **Coverage runs out.** 33 conforming deployments across three schema families and five networks. `yield-vault` is thin: two deployments on Ethereum, one on Arbitrum, none elsewhere. Where R3 has nothing to read, it says so. - **The Uniswap V3 factory is the slowest thing here.** R3's sample query for it — the largest pools by value locked — often does not finish inside the three-second gateway timeout, and a call to the factory then comes back `unavailable`; when its deployments are keeping up, it answers `low`. Either way the answer says which it was. Calls to the *pools* do not have the problem: they are checked by id and answer in about a hundred milliseconds warm. - **The incident registry lags.** ScamSniffer's open list trails its real-time data by seven days, so a drainer from this week is not on it. Its state is shown on `/health` under `sources` as `incident-registry`.