Skip to content

Enable Banking poller

Source: agent/src/accounting_agent/reconcile/enablebanking_poller.py. Runs as a background asyncio task inside the webhooks container.

What it does, in order

For each active Enable Banking session (one per linked bank account):

  1. GET /sessions/{id} to verify the session is still valid.
  2. For each account in the session: iter_transactions from (last_polled_at - OVERLAP_DAYS) to now.
  3. For each transaction:
  4. Convert to a NewReconciliation (one of debit/credit, with source_kind='wise', source_reference=<txn id>, plus the amount/date/currency/counterparty/description).
  5. Look up the document store for candidate matches in the date+currency window.
  6. Score the top candidate via the same matcher used elsewhere.
  7. If score ≥ HITL_THRESHOLD → insert with status='proposed' and document_id set. Otherwise → status='unmatched'.
  8. For existing rows with status='unmatched': re-score (the "re-match path") and promote to proposed if the score now clears.
  9. Update last_polled_at on the session.

Why polling, not webhooks

Enable Banking offers webhooks but only for paid tiers. The free PSD2 dev tier is poll-only. 4 polls/day × 30 days = 120 calls/month per account, well under the ~100 AIS calls/account limit at our overlap of 2 days.

Idempotency

(source_kind, source_reference) is UNIQUE on the reconciliations table. Re-running the poller produces the same end state. If the poller crashes mid-run, the next start re-fetches the same window and the UNIQUE constraint silently dedups.

The card-debit shape

Wise via PSD2 splits the card-debit description across two remittance lines:

["CARD-3976488961",
 "Card transaction of 45.61 EUR issued by Hetzner Online Gmbh HETZNER.COM/"]

The poller's _counterparty_name falls back to the joined remittance string (when creditor_account.name is empty) so the matcher's counterparty signal has something to score on. Otherwise card debits would score below HITL forever — see the matcher card-debit boost.

What it does NOT do

  • It doesn't book to e-Fin. That's the booker daemon.
  • It doesn't talk to Discord. The bot handles HITL prompts on the rows the poller writes.
  • It doesn't run vat_decision. VAT happens at book time, not pull time.