The AP 3-way match is the oldest AP control in the book. The PO says “we agreed to buy these 100 widgets for $5,000.” The vendor invoice says “we delivered the widgets and want $5,000.” The goods-receipt says “yes, 100 widgets arrived.” Match all three; pay. Disagree on any one; investigate.

The control survived because it works. It also survived because for decades it was painful to operate at scale — every invoice required manual lookup against PO + GR data. AI changes the operational cost; the control stays the same.

This article walks how to automate the 3-way match with AI while preserving the SoD chain that makes it audit-defensible.

The 5-step AP workflow with closegate

1. RECEIVED       → invoice arrives (email, EDI, portal)
2. CODED          → line items mapped to GL accounts
3. MATCHED        → 3-way match runs (PO ↔ invoice ↔ goods-receipt)
4. APPROVED       → human approves for payment
5. PAID           → payment run submits to bank

Each transition has a tier annotation. The agent does most of the work; humans confirm at the right points.

Step 1: RECEIVED (T0)

The agent ingests the invoice via ap_ingest_invoice (T0 — read-only persist). Vendor adapters handle the heterogeneity: Stripe, Plaid, Mercury for cards/banks; QuickBooks, NetSuite, Codat, Merge for ERPs. New adapters take ~150 LOC.

No human review at this step; the agent’s job is to normalize and persist.

Step 2: CODED (T1 with audit)

ap_propose_invoice_coding (T0) — the LLM proposes GL-account codings for each invoice line. It reads the line description, looks at historical coding for the same vendor, and proposes.

ap_confirm_invoice_coding (T1) — the controller (or the agent, with tier 1 auto-confirm enabled below materiality) confirms the codings. Below-materiality auto-confirm is reasonable here; coding errors are reversible up until the invoice is approved for payment.

Step 3: MATCHED (T0)

ap_match_three_way (T0) — the deterministic matcher runs against PO + invoice + GR. Three outcomes:

  • Match on all three within tolerance → flag as MATCHED, ready for approval
  • Match on PO + invoice but no GR yet → flag as PENDING_RECEIPT (goods haven’t arrived)
  • Tolerance breach on any field → flag as EXCEPTION + assign to AP manager

The matcher is deterministic — no LLM call. The LLM’s role at this step is suggesting why the exception happened (“vendor sent revised price; PO not yet amended”), not making the match decision itself.

Step 4: APPROVED (T2 — HITL required, SoD: actor ≠ PO requestor)

ap_approve_invoice_for_payment (T2) — a human approves the invoice. The gate enforces:

  • The approver must be a different actor identity than the PO requestor (SoD)
  • The approver must have the appropriate authority level (e.g., not a junior AP clerk on $50K invoices)
  • Sensitive-account routing fires if any line touches a sensitive account
  • Materiality threshold fires if the total exceeds the team’s threshold

This is the gate’s chokepoint moment. The LLM cannot approve; the runtime denies on source == llm AND actor.kind == llm. The approver is a human with an OIDC token tied to an actor identity.

Step 5: PAID (T3 — dual HITL, requestor ≠ approver ≠ payer)

ap_propose_payment_run (T2) — group APPROVED invoices into a draft payment run.

ap_approve_payment_run (T2) — controller approves the payment run. SoD: approver ≠ requestor.

ap_submit_payment_run (T3) — payment hits the bank/ACH. Dual HITL. Three distinct humans, three distinct actor identities. requestor ≠ approver ≠ payer. No prompt can override. No exception.

The runtime checks at submit time:

  • The payer actor must not be the approver
  • The payer actor must not be the requestor
  • All three actors must be authenticated humans (not engine, not llm)

If any check fails, the gate denies with SOD_SAME_ACTOR and writes a POLICY_VIOLATION event.

What the AI agent is good at

  • Invoice coding suggestions. The LLM proposes the GL account for each line based on description + vendor history. Accuracy is typically 85%+ on first attempt; humans correct the rest.
  • Duplicate detection. Vendor name normalization across spelling variants (“Acme Corp” vs “Acme Corporation” vs “ACME, Inc.”). The deterministic matcher handles exact duplicates; the LLM catches near-duplicates.
  • Exception explanation. When a tolerance breach hits, the LLM proposes a likely reason (“price increase since PO; vendor sent revised quote attached to invoice email”). The human still confirms.
  • Vendor bank-change detection. The LLM flags when a new bank account differs from prior payments to the same vendor — a sensitive-account routing always fires here.

What the AI agent is NOT allowed to do

  • Approve an invoice for payment. Always T2 minimum; always HITL.
  • Submit a payment run. Always T3; always dual HITL.
  • Override the materiality threshold. Threshold is policy.yaml; policy.yaml is git; PR review required.
  • Override the SoD check. The runtime enforces this regardless of LLM behavior.

The fraud vectors closegate’s AP flow defends against

  1. Vendor bank-change attack. Email from “vendor” requesting payment to new bank account. The gate routes vendor bank-change events to HITL regardless of materiality. The AP manager sees the change, calls the vendor on a known number, confirms.
  2. Duplicate invoice submission. Vendor submits the same invoice twice from variant email addresses. The LLM catches this via vendor identity normalization; the gate flags as exception.
  3. PO splitting. Invoice broken into two below-threshold pieces to avoid approval requirements. The gate aggregates by vendor + week + PO; flags totals over the threshold even if individual invoices are below.
  4. Phantom vendor. Invoice from a vendor never paid before. The LLM flags vendor-first-payment as exception; HITL routing fires.
  5. Same-actor approval-and-payment. The dual-HITL T3 chain prevents one actor from both approving the run and releasing to bank.

What this gives your AP team

  • 70%+ reduction in manual coding effort (LLM proposes; humans confirm)
  • 100% of payments cleared by an SoD chain that survives audit walkthrough
  • Tamper-evident audit log with verbatim policy clauses
  • Vendor bank-change attack defense by default
  • Reproducible PBC bundle via closegate-engine audit-evidence-export