Import a payroll run with every IBAN checked mod-97.
Payroll files carry salary and bank details in one row — the highest-sensitivity data most companies move. Map them to payroll_v1, have every IBAN checked by its own checksum, and get the review flag back on every run.
German headers from a Swiss payroll bureau, an amount formatted 8’450.00, and an IBAN column labelled simply Konto.
The job
What you are actually trying to get done
The payroll bureau exports a file every month and someone re-types it, or writes a one-off script that breaks when a column moves. The data is as sensitive as anything the company holds — salary next to a bank account, per employee. The job is to import it without hand-editing, catch a mistyped IBAN before money moves, and have a record of who approved the mapping.
Where it lands
Payroll
payroll_v1 · risk: high
The iban validator is mod-97 checksum-strict and country-scoped — this template accepts CH, LI, DE, FR, IT and ES. A transposed digit fails the checksum and is reported with its row index, which is the whole point of validating before a payment file is produced.
Excel is read natively and multi-sheet by lib/xlsx.ts — no SheetJS, no egress. The response lists every worksheet, so a workbook with a summary tab in front of the data is one re-POST away.
POST /v1/uploads
2
Map the German headers
Personalnummer, Bruttolohn, Lohnperiode and Steuercode are all shipped hints on this template. Konto is a hint on iban. Nothing here reaches a model.
POST /v1/uploads/:id/match
3
Validate the account numbers
The iban validator runs mod-97 over each value and rejects a country the template did not list. A failing row is named in errors with its field — before anything is committed.
POST /v1/uploads/:id/validate
4
Gate on the flag
payroll_v1 is high-risk, so the mappings response always carries hitl_status: "pending_review". Your importer reads that and holds the batch for a human — the flag is advisory, so the gate is yours to enforce.
requires_hitl: true
5
Deliver where it needs to go
Inline rows, an HMAC-signed webhook, a serialized file (csv, xlsx, parquet, sql …), or a direct write into a destination connector — combinable in one commit.
POST /v1/uploads/:id/commit
In code
Validate before money moves
The validate call is the one that matters here. It runs the template validators over every row and returns errors with their row index and field, capped at 500 so a bad file cannot return an unbounded response.
curl
curl -X POST https://api.adaptivmapr.com/v1/uploads/upl_3d91ab/validate \
-H "Authorization: Bearer $ADAPTIVMAPR_API_KEY"
→ 410 of 412 rows clean · 2 held back · 0 headers reached the LLM
The iban validator is mod-97 and country-scoped; bic is a separate type. Nineteen validator types ship, and the checksum-strict ones (iban, gtin, npi, gln) genuinely compute the check digit rather than pattern-matching it.
Salary and bank data is PII, not PHI. This job runs on standard routing by default and pays no surcharge — PHI routing is available if your data class calls for it, at +20% and behind the BAA.
Every commit writes an audit event to Supabase audit_logs scoped to your tenant_id. GET /v1/audit reads it back and POST /v1/me/audit/export produces a signed download token.
Post-response work — wallet debits, usage reporting, audit writes — is registered with the Worker’s ctx.waitUntil through afterResponse(), so it completes after the response is returned rather than being dropped when the isolate stops.
Where the work lands
Which layer resolves this file
Layer
What it does here
Auto-accepts at
Cost
1 · statistics
The bureau’s exact header row, once confirmed — every month after the first
≥100 @ 95% · ≥20 @ 100%
Free · deterministic
2 · heuristic
“Personalnummer”, “Bruttolohn”, “Lohnperiode”, “Steuercode”, “Konto” — all shipped hints
≥ 0.85
Free · deterministic
3 · fuzzy
“Währung” → currency after accents and punctuation are normalized away
≥ 0.80
Free · pure compute
4 · semantic
A cost-centre or contract label with no shared vocabulary
≥ 0.78
Cheap, cached · off without an embedding key
5 · ai
One batched call over anything left — usually nothing on a payroll export
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
payroll_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
Not in schema-only mode, which is the default: only the column headers and at most three sample rows of ≤80 characters are used to decide the mapping, and the rows themselves are transformed in-process. Full-data mode, which sends row values for AI cleanup, is a separate opt-in gated on the PHI entitlement.
The mod-97 checksum over the rearranged account identifier, plus the country prefix against the list the template declares — CH, LI, DE, FR, IT and ES on payroll_v1. It is a well-formedness and checksum check, not a confirmation that the account exists or that the name matches.
The requires_hitl flag is advisory in v1: it is returned on every mapping for a medium or high-risk template, but the upload is not server-side blocked. Your importer reads hitl_status and holds the batch. Native AgentGate approval-queue integration — a server-side gate — is on the roadmap for Q3 2026 and is not wired in v1.
A small flat per-map fee, around $0.001, drawn from a prepaid wallet you top up from $10. When every column resolves on the deterministic layers — the normal case for a file you get monthly — no model runs and there are no AI tokens to bill. There is no free tier and no subscription.