Accounts payable · Design, build, rolloutCuentas a pagar · Diseño, desarrollo e implementación

AP Control Tower

An accounts payable day is email, not software. I built AP Control Tower around that shape: the mailbox is the input, the exception queue is where human review belongs, and the payment proposal has to end up in the client's ERP. The first decision shaped every other one: the system has no way to execute a payment. It writes proposals, a person approves them, and the whole chain is logged. It has been in pilot with a Madrid-based consulting firm since July 2026, on a three-month engagement, and I run it as a managed service. El día de cuentas a pagar es correo, no software. Armé AP Control Tower con esa forma: la casilla es la entrada, la cola de excepciones es donde va la revisión humana, y la propuesta de pago tiene que terminar en el ERP del cliente. La primera decisión condicionó todas las demás: el sistema no tiene forma de ejecutar un pago. Escribe propuestas, una persona las aprueba y toda la cadena queda registrada. Está en piloto con una consultora con sede en Madrid desde julio de 2026, con una duración de tres meses, y lo opero como servicio gestionado.

01Email inboxCasilla de correoSupplier invoicesFacturas de proveedores
02ExtractionExtracción29 fields, null if absent29 campos, null si no está
03ControlsControlesDeterministic, in PythonDeterministas, en Python
04Exception queueCola de excepcionesHuman reviewRevisión humana
05Approval gateAprobaciónProposal file, not paymentPropuesta en archivo, no pago
StatusEstado
In pilot since July 2026, three-month engagementEn piloto desde julio de 2026, con una duración de tres meses
RoleRol
Design, build and implementation, end to end. I run it as a managed service.Diseño, desarrollo e implementación de punta a punta. Lo opero como servicio gestionado.
Stack
Python · Streamlit · Cloud SQL · Sage file integrationpor archivo
Cloud & AICloud e IA
Cloud Run · Google Document AI · Ollama

The problemEl problema

Supplier invoices arrive by email and someone has to read, key, check and pay each one. The failure modes are dull, and each one ends with money leaving: the same invoice paid twice, a proforma booked as final, a total that does not agree with base plus VAT, a tax ID with a check digit that does not validate, an IBAN that quietly changed between one payment run and the next. A careful person catches that on a good day. I did not want the catching to depend on the day.Las facturas de proveedores llegan por correo y alguien tiene que leer, cargar, controlar y pagar cada una. Los modos de falla son aburridos y todos terminan con plata que sale: la misma factura pagada dos veces, una proforma contabilizada como definitiva, un total que no coincide con base más IVA, un identificador fiscal cuyo dígito de control no cierra, un IBAN que cambió sin ruido entre una corrida de pagos y la siguiente. Una persona atenta lo detecta en un día tranquilo. No quería que la detección dependiera del día.

There is a second failure mode that belongs to the tooling rather than the person: a model asked to read a field that is blurry, cropped or simply absent will write something plausible, and a filled field does not ask to be checked.Hay una segunda falla que es de la herramienta y no de la persona: si a un modelo le pedís que lea un campo borroso, cortado o directamente ausente, escribe algo verosímil, y un campo completado no pide que lo revisen.

The approachEl enfoque

I split the system along a line that never moves: the model reads, the code decides. Anything that depends on a number being correct is Python, not a prompt. Anything that depends on reading a messy PDF is the document model, boxed into a schema. Classification (invoice, proforma, other) sits between them, so a proforma or an unrelated document is labelled as such before anything downstream treats it as an invoice. Whatever fails a control goes to the exception queue with the document rendered next to the extracted values. Whatever passes waits at the approval gate.Dividí el sistema por una línea que no se mueve: el modelo lee, el código decide. Todo lo que depende de que un número esté bien es Python, no un prompt. Todo lo que depende de leer un PDF desprolijo es el modelo documental, acotado por un esquema. La clasificación (factura, proforma, otro) está en el medio, así una proforma o un documento ajeno queda etiquetado como tal antes de que algo más abajo lo trate como factura. Lo que no pasa un control va a la cola de excepciones, con el documento renderizado al lado de los valores extraídos. Lo que lo pasa espera en la instancia de aprobación.

Control designDiseño de controles

  • Arithmetic is code, not prompt. Base plus VAT is recomputed and compared against the total printed on the document. If it does not agree, it is an exception, not a rounding note.La aritmética es código, no prompt. Base más IVA se recalcula y se compara contra el total impreso en el documento. Si no cierra, es una excepción, no una diferencia de redondeo.
  • Identity and repetition, checked in code. Spanish NIF/CIF and IBAN are validated by checksum, and duplicate detection runs on every document that comes in. These checks are exact, deterministic, and not something to hand to a model.Identidad y repetición, verificadas en código. NIF/CIF e IBAN se validan por dígito de control, y la detección de duplicados se aplica sobre cada documento que entra. Son verificaciones exactas, deterministas y que no hay por qué dejárselas a un modelo.
  • A change of supplier bank details is an event, not a field update. If the account differs from the one on file, the invoice stops and a person decides. A changed account is the one thing you cannot undo once the payment has left, so catching it does not depend on someone happening to notice.Un cambio en los datos bancarios de un proveedor es un evento, no la actualización de un campo. Si la cuenta difiere de la registrada, la factura se frena y decide una persona. Una cuenta cambiada es lo que no se puede deshacer una vez que el pago salió, así que frenarla no depende de que alguien se dé cuenta.

Extraction and evalsExtracción y evals

The schema has 29 fields and one rule that governs the rest: if a field is not visible on the document, it is null. No inference, no filling gaps from context, no plausible guesses. I kept the extraction engine behind an interface from the start, so the pipeline does not know whether Google Document AI or a local open weight model through Ollama produced the fields, and the same schema can run on premise, where there is no per-document cost. Because the engine can change, the evals cannot: golden labels, run automatically, with a comparator where nulls count as predictions and hallucinated values are reported apart from misses. A field invented out of nowhere is a different kind of error than a field left blank, and averaging the two together hides the one that can cost money.El esquema tiene 29 campos y una regla que gobierna al resto: si un campo no está visible en el documento, es null. Nada de inferir, nada de completar con contexto, nada de suposiciones verosímiles. Dejé el motor de extracción detrás de una interfaz desde el arranque, así el pipeline no sabe si los campos los produjo Google Document AI o un modelo open weight local vía Ollama, y el mismo esquema puede ejecutarse on premise, donde no hay costo por documento. Justamente porque el motor puede cambiar, las evals no: golden labels, ejecución automática y un comparador donde los nulls cuentan como predicción y los valores alucinados se reportan aparte de los no detectados. Un campo inventado de la nada es un error distinto de un campo en blanco, y promediarlos tapa justo el que puede costar dinero.

The design constraintLa restricción de diseño

The constraint came first: an AI system that cannot do the thing it sits closest to.La restricción vino primero: un sistema de IA que no puede hacer justo aquello que tiene más cerca.

It sits on the invoice inbox, it knows the amounts and it knows the bank details, and it has no way to move money. Approval is a human action, it is recorded, and nothing leaves the tool until it happens. On the data side, everything is processed as a processor under article 28 of the GDPR, with documented sub-processors and certified deletion when the engagement ends. I would rather build a system that asks than one that is usually right.Está montado sobre la casilla de facturas, conoce los importes y conoce los datos bancarios, y no tiene forma de mover dinero. La aprobación es un acto humano, queda registrada, y nada sale de la herramienta hasta que eso ocurre. Del lado de los datos, todo se trata en calidad de encargado del tratamiento según el artículo 28 del RGPD, con subencargados documentados y borrado certificado al terminar el trabajo. Prefiero construir un sistema que pregunta antes que uno que casi siempre acierta.