Concepts

Settlement

What happens after a fill today, and what is planned.

Current version. Settlement contracts are not deployed yet. Fills are recorded in a public, hash-chained ledger and no funds move. The protocol fee is TBD and is 0 in the current version. $FILO has not launched: there is no contract address, and supply, venue and staking thresholds are TBD.

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/verify recomputes the whole chain on the server and returns { ok, height, head }, plus broken_at if a check fails.
  • Each fill has a public receipt page in the app at /fills/:id that re-checks its hashes in your browser. The full ledger is public at /ledger in the app.

Hash rules

  • payload_hash = sha256hex(canonicalJson(payload)), where payload is the fill with exactly these fields: id, order_id, agent, provider_id, provider_name, max_price, price, saved, created_at. ledger_height and ledger_hash are not part of it.
  • canonicalJson sorts object keys at every level, adds no whitespace, serializes each value with JSON.stringify and drops keys whose value is undefined.
  • 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.

verify-ledger.mjs (Node 18+)
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.

Was this page helpful?