A small business in Brazil gets paid on two rails that never talk to each other. PIX lands in the bank. Dollars land on Solana. fecho is a self-hosted ZeroClaw agent that closes the books across both at the end of a day, then anchors the proof on-chain.
fecho de caixa is Portuguese for the daily close. The moment a shop stops selling and finds out if the books balance.
The agent holds no key. The operator signs the close.
A closed day ends as one ledger file. Its SHA-256 goes into a Solana memo transaction signed by the operator's own wallet, and every close names the hash of the close before it. A dated chain that a block explorer timestamps for you, so this agent's run history is not a claim in a README. It is public record.
| # | Date | Anchor |
|---|---|---|
| 0 | 2026-07-28 | 2fqWVcER…yvdWZ |
| 1 | 2026-07-29 | 5mhFQWXN…qdHDD |
| 2 | 2026-08-05 | 2kcP4DLq…f9RVE |
| 3 | 2026-08-06 | BAGeFuTW…znUGa |
One row per close, and a close happens when the operator runs it. The dates above are the dates that exist: 2026-07-30 through 2026-08-04 have no close, because the desk was not closed on those days while the rest of this repository was being built. The cron jobs that would make it nightly are written and not registered yet. We did not fill the gap in, and docs/SHOWCASE.md says why.
A chain proves nobody edited a close after it was signed. On its own it does not prove the close was right when it was signed — those are different claims, and confusing them is how a wrong number gets a proof attached. So a close carries its evidence too. Every settled invoice is one record naming the amount in base units, the verdict, and the transaction signature (dollars) or Pix end-to-end id (BRL) behind it. A rail's total has to equal the sum of those records or scripts/close.sh refuses the draft, offline, before anything reaches the ledger. The numbers in a close are checkable by anyone holding the file, against a block explorer, forever.
You can't backdate a blockchain.
Most agent demos show a payment happening. That part is easy to show and easy to stage. The part a real business needs is what happens at 23:00: which invoices got paid, which came up short, whether the bank statement agrees with the chain, and whether you can prove any of it next month.
flowchart TB
INV[invoices] --> USDC[dollars on Solana<br/>Solana Pay + reference key]
INV --> PIX[PIX in BRL<br/>copia e cola]
USDC --> REC[reconcile pass]
BANK[bank statement CSV] --> REC
PIX -.paid at the bank.-> BANK
REC --> CLOSE[the close<br/>ledger/YYYY-MM-DD.json]
CLOSE --> ANCHOR[sha256 in a memo tx<br/>operator signs]
ANCHOR --> SOL[(Solana mainnet)]
style REC fill:#14F195,color:#0b3d2c,stroke:#0ec27a
style CLOSE fill:#14F195,color:#0b3d2c,stroke:#0ec27a
style ANCHOR fill:#14F195,color:#0b3d2c,stroke:#0ec27a
style SOL fill:#0b3d2c,color:#14F195,stroke:#062418
The night, in order:
- A cron SOP wakes the agent. No webhooks, no public ingress, nothing listening. The two
[cron.*]jobs are written and in version control; on the reference deployment they are not registered yet, so the operator fires the SOPs by hand. - Every open invoice's reference key gets checked against finalized transactions. Amounts are compared in base units with u128 math. No
f32orf64exists anywhere in this codebase;uiAmountappears only inside recorded RPC fixtures and in the tests that prove it is never read — one of them sets it to99999.5and the verdict does not move. - The bank CSV is matched row by row on the PIX side. Duplicated end-to-end IDs get flagged, not merged.
- The close is written: per-rail totals, plus one record per settled invoice naming its verdict and the evidence behind it. It chains to yesterday's hash and is summarized to the operator.
scripts/close.shre-does the arithmetic before the file can enter the ledger. A draft that does not add up is refused and the ledger stays untouched.- The operator reviews and signs the anchor. The agent computes, the human signs. That line never blurs.
"Paid" is a verdict, not a feeling. Verification reads the finalized transaction and decides:
| Verdict | Meaning |
|---|---|
paid |
exact amount, right mint, right recipient, finalized |
overpaid / underpaid |
amount mismatch, measured in base units |
fee_legit_short |
a Token-2022 transfer fee explains the exact shortfall. A fee excuses exactly the fee |
wrong_asset |
right recipient, wrong mint |
wrong_mint_dust |
a dust transfer of a junk mint dressed up as a payment |
no_credit |
reference present, funds went somewhere else |
failed |
landed but errored |
unfinalized |
not final yet, so not money yet |
unverifiable |
anything malformed. Never resolves to paid |
The PIX side gets the same treatment: unmatched statement rows, duplicate end-to-end IDs, and BRL/USD drift are first-class flags in the close, because the failure modes of a bank export are not the failure modes of a blockchain.
T1, build only. Secrets held: none. Not a policy the model is asked to follow. A shape it cannot express a violation in.
No tool here accepts a destination address. A refund can only return to the wallet the chain already recorded as the sender, derived from the payment's own token balance changes. When a prompt injection supplies an address, there is no parameter for it to land in.
Refund transactions ride a durable nonce. An unsigned transaction built on a recent blockhash dies in about ninety seconds, usually while the human it waits for is still reading it. On a durable nonce, the transaction lives exactly as long as the approval does.
| Capability | Who holds the key |
|---|---|
| invoices (Solana Pay URL, PIX payload) | nobody, they are strings |
| verification | nobody, read-only RPC |
| refunds | the operator signs. The agent only builds, unsigned |
| the daily anchor | the operator signs |
Eight prompt-injection attacks were run against the live agent and written up
in docs/attacks/: refund redirect, forged operator
override, poisoned payment data, settlement by assertion, config
exfiltration, key extraction, the refund cap, and self-approval. Seven were
prevented structurally. The eighth was recorded PARTIAL, and our own
review is what found it: close.sh validated a close draft's schema, date,
and hash chain but never its numbers, so a fabricated draft chained
correctly. That gap is closed in code now. Schema fecho-close/1 makes every
claimed base unit name its evidence, validate_totals in
core/src/ledger.rs refuses a close that does not add
up, and close.sh runs it before anything is copied into ledger/. The
fabricated draft has been handed back to close.sh and is refused three
ways, with ledger/ untouched. It stays PARTIAL rather than PREVENTED for
a smaller reason, stated in the file: arithmetic on a file cannot reach the
chain, so a close that forges a well-formed signature and makes its totals
agree still validates. Each file carries the attacker's exact message, the
full exchange, and the file and line that stopped it — including the times the
model said something wrong while the transaction stayed correct.
rustup target add wasm32-wasip2
./scripts/verify.shfecho verify
core
ok 242 tests
ok clippy -D warnings
ok no floats in core
plugins
ok fecho-invoice builds for wasm32-wasip2
ok fecho-ping builds for wasm32-wasip2
ok fecho-refund-build builds for wasm32-wasip2
ok fecho-verify builds for wasm32-wasip2
ledger
ok 2026-07-28.json 08f639c498bab0dd80bc6a4461f7a278b348ab26c480289dd1a2e43a1cd4dc6f
ok 2026-07-29.json 7e264b8c8e79e8afb2d61336b778a1142755c1c46c14b6b859117d408bfcdd1f
ok 2026-08-05.json 9f0e974cd82daa172777dc6d7ea5cc6e96d09a9ce541cebc04f2db3900e279d7
ok 2026-08-06.json 5ead436a875f8c45d394c8b7203d917ed90881363a64d033d844103afad08304
ok anchor 2026-07-28 matches on-chain hash
ok anchor 2026-07-29 matches on-chain hash
ok anchor 2026-08-05 matches on-chain hash
ok anchor 2026-08-06 matches on-chain hash
all checks passed
No network, no keys. The script re-derives every close's hash from the file on disk and checks it against what the anchor transaction recorded, and re-adds every close's settlements to check they still equal the totals that close declares. If anyone edits history, this is the command that catches them.
One invoice has gone the whole way on mainnet: issued by the desk, paid by an independent program with its own key, then found on chain and called paid by the desk itself. Command by command, with the paying transaction decoded by hand, in docs/MONEY_LOOP.md.
Getting the full agent running (source-built host, plugin install, channel, skill, SOPs) is in docs/RUNBOOK.md, written so a stranger gets there in an evening. docs/BOUNDARY_NOTES.md records what the wasm component boundary actually does when you meet it for real: the wit drift between the registry and the host, the three silent gates between an installed plugin and a tool the model can see, the HTTPS port that vanishes inside the sandbox. Each of those cost somebody an evening.
core/ pure Rust: money math, verification, PIX, Solana Pay, the close
242 tests. no network, no wasm, no clocks, no floats
plugins/ wasm32-wasip2 tool components, thin shims over core
fecho-invoice · fecho-verify · fecho-refund-build · fecho-ping
wit/v0/ plugin ABI, vendored from the exact host revision we build
ledger/ the live close series + anchors.jsonl
skills/fecho/ the desk's own rules, installed as skill bundle "desk"
sops/ fecho-reconcile · fecho-close · fecho-refund
config/ config.snippet.toml, the operator config template
fixtures/ synthetic bank statement exports for the PIX rail
tools/payer/ a customer that pays an invoice. Not a plugin, not installed,
not reachable from a chat message
scripts/ close.sh · verify.sh · anchor-close.sh · refbook.sh
ingest-statement.sh
docs/ RUNBOOK.md · OPERATIONS.md · MONEY_LOOP.md · EVIDENCE.md
SHOWCASE.md · ONE_PAGER.md · BOUNDARY_NOTES.md · VIDEO.md
attacks/ · upstream/
| Instead of | fecho does |
|---|---|
| a chat message summarizing the day from mutable memory | a hash-chained file an accountant can hold |
| a hash chain, and calling "nobody edited this" the same as "this was true" | settlements with their evidence inside the close, and a script that refuses a draft whose totals do not equal the sum of them |
| a refund tool with an address argument and an approval prompt guarding it | no address argument at all |
| recent blockhashes and hoping the human signs fast | durable nonces |
uiAmount floats |
u128 base units, parsed from amount strings |
| dumping RPC responses into the model's context | shaped output under 512 bytes, asserted by tests |
| everything as WASM plugins | skills and SOPs on the stock feature set, compiled code only where determinism demands it |
| Dependency | What it could do | What contains it |
|---|---|---|
| Solana RPC | serve stale or false reads | finalized only, malformed input never resolves to paid |
| bank CSV | be wrong or tampered | operator-supplied evidence, hashed into the close, never proof on its own |
| model provider | see everything the agent sees | it cannot move funds. No tool signs |
Built in public for the Superteam Brasil × ZeroClaw Solana bounty.
| Component | State |
|---|---|
| core (money math, verification, PIX, Solana Pay, ledger) | done, 242 tests |
| fecho-invoice, fecho-verify, fecho-refund-build | done, 142 tests, live on a real host |
| skill, SOPs, Telegram channel | installed: bundle desk claimed by the agent, three SOPs registered, Telegram healthy with an allowlist of one |
| schedules | written as [cron.*] jobs, not registered on the host yet. The SOPs are fired by hand |
| close series | live: four closes anchored, 2026-07-28, 2026-07-29, 2026-08-05 and 2026-08-06. Not consecutive, and the gap is stated rather than filled |
| money loop | proven on mainnet: invoice issued, paid by an outside key, verified paid by the desk |
| refund path | proven: a real unsigned refund built on a durable nonce |
| PIX rail | code done and fixture-tested. No BRL cent has settled through it |
MIT licensed. ZeroClaw is MIT/Apache-2.0 and community-maintained, its only official repository is zeroclaw-labs/zeroclaw. fecho is a third-party community project, not part of ZeroClaw core.