Master data + HITL
There are two kinds of writes the agent does to e-Fin:
- Transactional writes —
purchase_invoiceentries, journal lines. High volume, ephemeral (each one is one transaction). These are autonomous. - Master data writes —
clientrecords (suppliers/customers),bank_accountrecords,purchase_articlerecords. Low volume, permanent (each one is referenced by every subsequent transaction). These are all HITL-gated.
This page explains why.
The duplicate-supplier problem
OCR is noisy. "Hetzner Online GmbH" / "Hetzner Online Gmbh" /
"HETZNER ONLINE GMBH" / "Hetzner DE" — the same vendor extracted
four ways from four invoices. If the agent silently created a
client record for each, the e-Fin books would have four duplicate
suppliers, all with the same VAT id but four clients_id values.
Every subsequent transaction would pick one at random based on
OCR exact-match. Cleanup is an accountant's nightmare.
So: client creation is always HITL. The operator sees the proposed
record, fixes typos if needed, and clicks Approve. The same vendor
on the next invoice finds the now-existing record via
find_by_name (case-insensitive, whitespace-collapsed). One
prompt per vendor, not per invoice.
The four master-data records
Suppliers (clients with is_supplier=true)
Gate fires: find_by_name(vendor_name) returns no match.
Resolver: POST /v1/clients with the proposed payload, then
re-run book_one(recon_id). Status: shipping (Phase 1).
Customers (clients with is_client=true)
Gate fires: sale-invoice booking hits a customer not in e-Fin. Resolver: same shape as supplier, flags inverted. Status: Phase 4 — gated until Stripe restarts.
Bank accounts
Gate fires: new IBAN appears via Enable Banking, OR booker
needs to write a transaction with a bank_accounts_id that's not
in e-Fin yet. Resolver: POST /v1/bank_accounts.
Status: Phase 2 — not blocking yet; the booker currently only
writes invoices, not transactions.
Purchase articles
Gate fires: OCR vendor's expense doesn't map to any existing
purchase article. Resolver: POST /v1/purchase_articles with
operator-confirmed account + dimension. Status: Phase 3 —
rare in practice; the fallback article 24 (Services purchased from
EU) covers most EU SaaS / hosting / professional services.
The HITL pattern (same for all four)
flowchart LR
A["Booker daemon<br/>hits missing record"] --> B{"Checkpoint<br/>already pending?"}
B -->|Yes| C["Skip silently<br/>WAITING_CLIENT_APPROVAL"]
B -->|No| D["Open checkpoint with<br/>proposed payload"]
D --> E["Bot poller picks it up<br/>posts Discord prompt"]
E --> F{"Operator"}
F -->|Approve| G["Resolver<br/>POST + re-book"]
F -->|Reject| H["Reconciliation<br/>marked rejected"]
The "checkpoint already pending" check is critical — the booker
daemon ticks every 15 minutes, and without this guard each tick
would spawn a new Discord prompt for the same recon. The check
walks pending checkpoints, decodes the payload, matches
reconciliation_id. Idempotent.
What the operator sees
A Discord message:
🏢 Create new e-Fin supplier? • Name: Hetzner Online GmbH • Country: DEU • Needed to book recon #44. Approve creates the supplier via API + continues the booking. Reject marks the recon rejected. [✓ Approve & create] [✗ Reject]
One click. The supplier is created, the booking continues, the bot
replies ✅ Created supplier #X and booked recon #N to e-Fin
entry #Y. Total operator time: under 5 seconds.