How to Build a compliance checking Agent Using LangChain in Python for payments
A compliance checking agent for payments reviews transaction data, merchant details, and policy rules before money moves. Its job is to flag risky or non-compliant activity early, produce an auditable decision trail, and reduce the chance of violating AML, sanctions, PCI-DSS, or internal policy.
Architecture
- •Transaction intake
- •Accepts payment payloads like amount, currency, country, merchant category, customer identifiers, and timestamps.
- •Policy/rules layer
- •Encodes hard controls such as sanctions screening thresholds, country restrictions, velocity limits, and KYC requirements.
- •Retriever for compliance context
- •Pulls relevant policy snippets, SOPs, and regulatory guidance from a vector store using
VectorStoreRetriever.
- •Pulls relevant policy snippets, SOPs, and regulatory guidance from a vector store using
- •LLM reasoning layer
- •Uses a LangChain
ChatOpenAImodel to classify the transaction against policy context and explain the decision.
- •Uses a LangChain
- •Decision formatter
- •Forces structured output with
PydanticOutputParserso downstream systems can consumeapprove,review, orblock.
- •Forces structured output with
- •Audit logging
- •Persists the input payload, retrieved evidence, model output, and final decision for regulators and internal audit.
Implementation
1) Define the compliance schema
Use a strict output schema. Payments systems need deterministic outputs because free-form text is useless in a rules engine.
from typing import Literal
from pydantic import BaseModel, Field
class ComplianceDecision(BaseModel):
decision: Literal["approve", "review", "block"] = Field(
description="Final compliance action for the payment"
)
reason: str = Field(description="Short explanation tied to policy")
policy_refs: list[str] = Field(description="Policy IDs or section references")
2) Load policy context into a retriever
For production you would load your policies from approved internal documents. The key point is that the agent should cite retrieved policy text instead of inventing answers.
from langchain_core.documents import Document
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
docs = [
Document(
page_content="POL-AML-001: Transactions over $10,000 require enhanced review.",
metadata={"policy_id": "POL-AML-001"}
),
Document(
page_content="POL-SAN-004: Payments involving sanctioned countries must be blocked.",
metadata={"policy_id": "POL-SAN-004"}
),
]
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(docs, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
3) Build the LangChain prompt and chain
This pattern uses ChatPromptTemplate, PydanticOutputParser, and RunnablePassthrough. The model gets the transaction plus retrieved policy context and returns structured JSON-like output.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.runnables import RunnablePassthrough
parser = PydanticOutputParser(pydantic_object=ComplianceDecision)
prompt = ChatPromptTemplate.from_messages([
("system",
"You are a payments compliance checker. "
"Use only the provided policy context. "
"If sanctions or prohibited geography appear, block. "
"If risk is unclear or thresholds are near limits, review."),
("human",
"Transaction:\n{transaction}\n\n"
"Policy context:\n{context}\n\n"
"{format_instructions}")
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def format_docs(docs):
return "\n\n".join(
f"[{d.metadata.get('policy_id', 'UNKNOWN')}] {d.page_content}"
for d in docs
)
chain = (
{
"transaction": RunnablePassthrough(),
"context": retriever | format_docs,
"format_instructions": lambda _: parser.get_format_instructions(),
}
| prompt
| llm
| parser
)
4) Run a payment through the agent
Keep the transaction object small and normalized. Never send raw card data to an LLM unless you have a very specific approved design; tokenize first.
transaction = {
"transaction_id": "txn_10021",
"amount_usd": 12500,
"currency": "USD",
"merchant_country": "US",
"customer_country": "US",
"merchant_category": "crypto_exchange",
}
result = chain.invoke(transaction)
print(result.model_dump())
A typical result might look like:
{
"decision": "review",
"reason": "Amount exceeds enhanced review threshold under POL-AML-001.",
"policy_refs": ["POL-AML-001"]
}
Production Considerations
- •Keep hard rules outside the model
- •Sanctions hits, blocked geographies, and PCI-sensitive checks should be enforced in deterministic code before or after the LLM call.
- •Log every decision path
- •Store input payload hashes, retrieved policy IDs, model version, prompt version, and final output in an immutable audit store.
- •Control data residency
- •If payment data must stay in-region, deploy embeddings/vector stores and model endpoints inside the required jurisdiction.
- •Add human review queues
- •Anything ambiguous should route to an analyst with full evidence: transaction data, matched policies, and model rationale.
Common Pitfalls
- •Using the LLM as the source of truth
- •Don’t ask it to “decide compliance” without retrieval and hard rules. Use it for classification and explanation over approved policy content.
- •Sending raw PANs or sensitive identifiers
- •Tokenize card numbers, mask account data, and strip unnecessary PII before calling LangChain components.
- •Ignoring auditability
- •If you cannot reproduce why a transaction was blocked or reviewed six months later, your design is not production-ready.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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