Concepts
Settlement
What happens after a fill today, and what is planned.
Today: the public ledger
Every fill is appended to a hash-chained ledger that anyone can read at GET /v1/ledger. Each entry links to the one before it, so changing or removing a past fill would break every hash after it.
GET /v1/ledger/verifyrecomputes the whole chain on the server and returns{ ok, height, head }, plusbroken_atif a check fails.- Each fill has a public receipt page in the app at
/fills/:idthat re-checks its hashes in your browser. The full ledger is public at/ledgerin the app.
Hash rules
payload_hash = sha256hex(canonicalJson(payload)), wherepayloadis the fill with exactly these fields:id,order_id,agent,provider_id,provider_name,max_price,price,saved,created_at.ledger_heightandledger_hashare not part of it.canonicalJsonsorts object keys at every level, adds no whitespace, serializes each value withJSON.stringifyand drops keys whose value isundefined.hash = sha256hex(prev_hash + payload_hash): the two lowercase hex strings joined as text, hashed, output as lowercase hex.- The first entry (genesis) uses 64 zeros as
prev_hash.
Check the chain yourself
This script walks the ledger from genesis, fetches each fill, recomputes its payload_hash and checks every link. It needs no key.
import { createHash } from "node:crypto";
const API = "https://dapp.filobook.run";
const sha256hex = (s) => createHash("sha256").update(s).digest("hex");
const canonicalJson = (v) =>
Array.isArray(v)
? `[${v.map(canonicalJson).join(",")}]`
: v && typeof v === "object"
? `{${Object.keys(v)
.filter((k) => v[k] !== undefined)
.sort()
.map((k) => `${JSON.stringify(k)}:${canonicalJson(v[k])}`)
.join(",")}}`
: JSON.stringify(v);
const payloadOf = ({ id, order_id, agent, provider_id, provider_name, max_price, price, saved, created_at }) =>
({ id, order_id, agent, provider_id, provider_name, max_price, price, saved, created_at });
let prev = "0".repeat(64);
let height = 0;
for (;;) {
// ?after=<height> returns entries oldest first, so we can walk from genesis.
const { entries } = await fetch(`${API}/v1/ledger?after=${height}&limit=1000`).then((r) => r.json());
if (!entries.length) break;
for (const e of entries) {
const fill = await fetch(`${API}/v1/fills/${e.ref}`).then((r) => r.json());
if (e.height !== height + 1) throw new Error(`gap at ${e.height}`);
if (e.prev_hash !== prev) throw new Error(`broken link at ${e.height}`);
if (sha256hex(canonicalJson(payloadOf(fill))) !== e.payload_hash) throw new Error(`payload mismatch at ${e.height}`);
if (sha256hex(e.prev_hash + e.payload_hash) !== e.hash) throw new Error(`bad hash at ${e.height}`);
prev = e.hash;
height = e.height;
}
}
console.log("ledger ok", { height, head: prev });Planned: on-chain settlement
The design is for filled orders to settle on Robinhood Chain (chain id 4663, gas in ETH) in USDG, with each fill receipt linking the request, the winning quote and the settlement transaction on the explorer. The contracts are not deployed and have not been audited. Audits will be listed when they exist.
Fees
The business model is a protocol fee on filled orders plus verification and listing fees for providers. All rates are TBD. The protocol fee is 0 in the current version.
Delivering the call
A fill names the winning provider and price. The v1 API does not proxy the call to the provider's endpoint or pay it over x402. Wiring that in is part of the settlement work. Until then, how the agent calls the winning endpoint is between the agent and the provider.