How to Build a document extraction Agent Using LlamaIndex in TypeScript for payments

By Cyprian AaronsUpdated 2026-04-21
document-extractionllamaindextypescriptpayments

A document extraction agent for payments takes messy inputs like invoices, remittance advices, bank statements, and payment instructions, then turns them into structured fields your downstream systems can trust. That matters because payments workflows fail in boring ways: wrong beneficiary, mismatched invoice numbers, missing tax IDs, or a bad currency code that slips through and creates reconciliation pain.

Architecture

  • Document ingestion layer

    • Pull PDFs, images, and office docs from object storage, SFTP, or a queue.
    • Normalize the file into text using an OCR or parsing service before LlamaIndex sees it.
  • LlamaIndex extraction layer

    • Use Document objects and a structured output pipeline.
    • Parse extracted text into a strict schema with OpenAI + extract/structured response patterns.
  • Payments schema layer

    • Define a TypeScript schema for fields like invoiceNumber, amount, currency, beneficiaryName, iban, swiftCode, and dueDate.
    • Keep optional fields explicit so missing data is visible, not silently inferred.
  • Validation and rules engine

    • Validate extracted values against payments rules: currency format, IBAN checksum, amount precision, date logic, supplier master data.
    • Reject or flag low-confidence records for human review.
  • Audit and traceability layer

    • Store source document hash, model version, prompt version, extracted JSON, validation results, and reviewer overrides.
    • This is non-negotiable in regulated payment flows.

Implementation

  1. Install the dependencies and define the extraction schema

Use LlamaIndex’s TypeScript SDK with a strict object shape. For payments, I prefer Zod because it gives you runtime validation and a clean contract for downstream services.

npm install llamaindex zod
import { z } from "zod";

export const PaymentDocumentSchema = z.object({
  documentType: z.enum(["invoice", "remittance_advice", "bank_statement", "payment_instruction"]),
  invoiceNumber: z.string().optional(),
  supplierName: z.string(),
  beneficiaryName: z.string().optional(),
  amount: z.number(),
  currency: z.string().length(3),
  dueDate: z.string().optional(),
  iban: z.string().optional(),
  swiftCode: z.string().optional(),
  taxId: z.string().optional(),
  purchaseOrderNumber: z.string().optional(),
});

export type PaymentDocument = z.infer<typeof PaymentDocumentSchema>;
  1. Load the document into LlamaIndex

LlamaIndex works on Document objects. In production you should OCR first if the source is scanned PDF; here I’m assuming you already have extracted text from your ingestion pipeline.

import { Document } from "llamaindex";

export function buildDocument(text: string, sourceId: string) {
  return new Document({
    text,
    metadata: {
      sourceId,
      domain: "payments",
      region: "eu-west-1",
    },
  });
}
  1. Extract structured payment fields with an LLM-backed query engine

The practical pattern is:

  • create an index from the document,
  • ask for a strict JSON response,
  • validate the result with Zod,
  • send failures to review.
import { OpenAI } from "llamaindex";
import { Document, VectorStoreIndex } from "llamaindex";
import { PaymentDocumentSchema } from "./schema";

const llm = new OpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

export async function extractPaymentFields(text: string) {
  const doc = new Document({ text });

  const index = await VectorStoreIndex.fromDocuments([doc]);
  const queryEngine = index.asQueryEngine({
    llm,
    similarityTopK: 3,
    responseMode: "compact",
  });

  const prompt = `
Extract payment document fields as strict JSON with these keys:
documentType, invoiceNumber, supplierName, beneficiaryName,
amount, currency, dueDate, iban, swiftCode, taxId, purchaseOrderNumber.

Rules:
- Return only JSON.
- Use null for missing optional values.
- amount must be numeric.
- currency must be ISO-4217 uppercase.
`;

  const response = await queryEngine.query({ queryStr: prompt });
  const rawText = String(response);

  const parsed = JSON.parse(rawText);
  return PaymentDocumentSchema.parse(parsed);
}
  1. Add business validation before posting to AP or payment rails

LLM output is not enough. Payments systems need deterministic checks before any booking or payout happens.

import { extractPaymentFields } from "./extract";

function validatePaymentDoc(doc: any) {
  if (doc.amount <= 0) throw new Error("Invalid amount");
  if (!/^[A-Z]{3}$/.test(doc.currency)) throw new Error("Invalid currency");
}

async function run() {
  const extracted = await extractPaymentFields(`
    Invoice No INV-1042
    Supplier Acme Supplies Ltd
    Amount EUR 1250.50
    Due Date 2026-05-01
    IBAN DE89370400440532013000
    SWIFT COBADEFFXXX
  `);

  validatePaymentDoc(extracted);
}

Production Considerations

  • Keep data residency explicit

    • Route EU payment documents to EU-hosted infrastructure and EU-approved model endpoints.
    • Don’t let OCR text or extracted payloads drift across regions by accident.
  • Log every decision

    • Persist the original document hash, extracted JSON, validation errors, human overrides, and model config.
    • Auditors will ask why a payment was classified a certain way; “the model said so” is not an answer.
  • Use confidence gates

    • Auto-process only when all required fields are present and validations pass.
Examples of hard stops:
- Missing beneficiary name
- Invalid IBAN checksum
- Amount mismatch vs PO tolerance
- Supplier name not found in master data
  • Treat PII as sensitive by default
Mask in logs:
- account numbers
- tax IDs
- addresses
- invoice line items if they contain personal data

Common Pitfalls

  1. Using free-form extraction instead of strict schemas

    • Problem: you get almost-right JSON that breaks downstream posting.
    • Fix: always parse into a typed schema with Zod or equivalent before any business action.
  2. Skipping deterministic validation

    • Problem: the model extracts “EUR” and “1250.5”, but the bank file requires exact decimal formatting and checksum validation.
    • Fix: run rule-based validators after extraction for IBANs, SWIFT codes, amounts, dates, and supplier match checks.
  3. Ignoring audit requirements

    • Problem: you cannot explain why a payment was approved or rejected.
    • Fix: store source document fingerprint, prompt version, model version, extracted payload, validation outcome, and reviewer action in immutable logs.

If you build this as an extraction-and-validation pipeline instead of a chat bot with side effects later on work queues will stay sane. In payments systems that’s the difference between automation that gets approved by risk and automation that gets shut down after the first incident.


Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides