Import a bank account file and catch the bad IBANs first.
A supplier or counterparty account list is a payment incident waiting to happen. Map it to bank_accounts_v1, have every IBAN checked mod-97 and every BIC checked by format, and get the review flag on every run.
A file assembled by hand over years — a mix of IBANs and legacy account numbers, a BLZ column that is sometimes a routing number and sometimes blank, and at least one IBAN with a transposed digit.
The job
What you are actually trying to get done
Before you can pay anyone you have to load their account details, and a wrong IBAN does not fail politely — it either bounces days later or pays the wrong account. The job is to get a hand-maintained list into one shape and have every identifier checked by its own checksum before it reaches a payment file.
Where it lands
Bank accounts
bank_accounts_v1 · risk: high
iban and bic are two distinct validator types. iban computes mod-97 and checks the country against this template’s list (CH, LI, DE, FR, IT, ES); bic checks BIC format. Legacy account_number and routing_number are carried as strings — they have no universal checksum to enforce.
CSV, Excel, JSON, XML, Parquet or a SQL dump. All parsed in-process — no bytes leave to read a tabular file, in either mode.
POST /v1/uploads
2
Map the German banking vocabulary
Kontoinhaber → account_holder, SWIFT → bic, Kontonummer → account_number, BLZ → routing_number — all shipped hints, all layer 2, all free.
POST /v1/uploads/:id/match
3
Run the checksums
Every IBAN is checked mod-97 and against the country list; every BIC by format. The transposed digit fails here, in a response with a row index, rather than in a bank rejection three days later.
POST /v1/uploads/:id/validate
4
Hold the batch for a human
bank_accounts_v1 is high-risk, so the mapping always comes back pending_review. That is a property of the template, not a setting — it applies on every call, in every workspace.
requires_hitl: true
5
Deliver the clean set
skip_invalid_rows: true commits what passed and returns the rest in skipped. Output as rows, a file, a signed webhook, or a direct write into your database.
POST /v1/uploads/:id/commit
In code
One stateless row check
POST /v1/validate-row is a stateless helper — no upload, no session — for checking a single record against a template’s validators. It is one of the two no-key MCP tools.
→ iban checked mod-97 · bic checked by format · no data stored, no key required
Nineteen validator types ship. The checksum-strict ones — iban (mod-97), gtin (GS1 check digit), npi (Luhn), gln (13-digit GS1) — actually compute the check digit rather than matching a pattern.
bank_accounts_v1 is one of five high-risk templates. The other four are payments_v1, kyc_profiles_v1, employees_v1 and payroll_v1.
Connector secrets — a treasury API token, a database credential — are KEK-encrypted at rest and readable only through the service-role plane, never through the user-JWT dashboard plane.
The rate limiter is two-tier: a shared Postgres counter is authoritative per fixed window, with a per-isolate in-process map as a fast pre-deny and a fail-open fallback when the shared store is unreachable.
Where the work lands
Which layer resolves this file
Layer
What it does here
Auto-accepts at
Cost
1 · statistics
Header→field pairs confirmed on earlier counterparty loads
“IBAN Nr.”, “Acct #” and other punctuated variants after normalization
≥ 0.80
Free · pure compute
4 · semantic
A treasury-system label with no shared vocabulary
≥ 0.78
Cheap, cached · off without an embedding key
5 · ai
One batched call over anything left, told which fields are already claimed
Model pick, constrained to the unclaimed column set
Metered · the only paid layer
Layers run in strict cost order and short-circuit — the first layer that resolves a column claims the target field and no later, dearer layer sees it. Layers 4 and 5 are config-gated (MAPR_EMBEDDING_API_KEY, MAPR_LLM_API_KEY) and fail soft to OFF, so an unconfigured deployment still maps on the deterministic layers rather than erroring.
Before you commit
Review, cost and where it routes
It comes back flagged for review
bank_accounts_v1 is a high-risk template, so PATCH /v1/uploads/:id/mappings returns requires_hitl: true and hitl_status: "pending_review" on every call, for every workspace, with nothing to configure.
Honest limit · The flag is derived from the template’s risk field and is advisory in v1 — we set it, your workflow owns the queue. The upload is not server-side blocked. A native AgentGate approval queue is roadmap, not v1.
Usually just the flat fee
When every column resolves on layers 1–3 — the common case for a file you receive regularly — no model runs, so there are no AI tokens to bill. A small flat per-map fee ($0.0010) is drawn on every map — a fully deterministic one and a layout-cache re-map included. There is no free tier and no subscription; you top up a prepaid wallet from $10 and it draws down.
Rate card · AI tokens are billed only when a model actually ran, at provider cost ×2 (×0.5 with your own LLM key). A PHI-routed run multiplies the whole charge — flat fee included — by 1.2. See lib/pricing.ts and GET /v1/pricing.
Standard routing, region still pinned
This job carries no protected health information, so it runs on standard routing with no surcharge. The workspace region pin still applies — the routing class picks the model catalogue; the region decides where compute may run.
Mechanism · resolveRunPhi() in lib/agreements.ts resolves the axis per run from phi_mode in the body, falling back to tenants.phi_mode (default false). PHI is available on this job too if your data class calls for it — it costs +20% and needs the BAA.
Questions
The things people actually ask
No. mod-97 proves the number is internally consistent — it catches transposition and typing errors, which is the overwhelming majority of bad IBANs in a hand-maintained list. It does not confirm the account is open, that the holder name matches, or that the bank will accept a payment. Account verification is a bank service, not a checksum.
No. bank_accounts_v1 declares iban, account_number and routing_number as separate fields, so a legacy row maps to the legacy fields and carries no IBAN. Only the iban field is checksum-validated; account_number and routing_number are strings, because there is no universal checksum to enforce across national schemes.
Yes — save it as a connector. sql_http posts a stored query to a SQL-over-HTTP endpoint and unwraps the rows; https and url_import fetch a file on a schedule with If-Modified-Since for incremental pulls; s3, gcs and azure_blob list a prefix and take the newest object since the watermark. A sync lands the grid as an upload and runs the deterministic cascade when the connector names a template.
Only masked. payments_v1 maps the last four digits and the brand — a full PAN must never be imported, and the template is built that way on purpose. Keep cardholder data out of scope; that is the whole design of the finance pack.