How to Build a underwriting Agent Using CrewAI in Python for payments

By Cyprian AaronsUpdated 2026-04-21
underwritingcrewaipythonpayments

An underwriting agent for payments evaluates a merchant, transaction, or payout request and decides whether to approve, review, or reject it based on risk signals. In practice, that means pulling together KYC/KYB data, transaction history, chargeback patterns, sanctions checks, and policy rules so you can make faster decisions without giving up auditability.

Architecture

  • Input normalizer

    • Takes merchant profile, payment method details, volume estimates, geography, and business category.
    • Converts messy upstream payloads into a strict schema before the agent sees anything.
  • Risk data collector

    • Pulls structured facts from internal systems: fraud scores, disputes, prior declines, velocity limits, and compliance flags.
    • Keeps external calls out of the reasoning layer.
  • Policy evaluator

    • Encodes underwriting rules for payments: MCC restrictions, country blocks, reserve requirements, and prohibited business types.
    • Produces deterministic constraints the agent must respect.
  • CrewAI decision agent

    • Uses the collected evidence to recommend approve / review / reject.
    • Writes a concise rationale with evidence references for audit trails.
  • Audit logger

    • Persists inputs, outputs, tool calls, timestamps, and policy version.
    • Needed for disputes, model governance, and regulator review.
  • Human review handoff

    • Routes borderline cases to an analyst queue with the exact reason codes.
    • Prevents the agent from making final calls on ambiguous or high-risk cases.

Implementation

1) Install CrewAI and define your underwriting schema

For payments, do not pass free-form JSON straight into the agent. Define a typed input object first so your downstream logic stays stable when upstream systems change.

from pydantic import BaseModel, Field
from typing import Literal

class UnderwritingInput(BaseModel):
    merchant_name: str
    country: str
    mcc: str
    monthly_volume_usd: float = Field(ge=0)
    avg_ticket_usd: float = Field(ge=0)
    chargeback_rate: float = Field(ge=0)
    fraud_score: float = Field(ge=0, le=1)
    kyc_status: Literal["pass", "review", "fail"]
    sanctions_hit: bool

2) Create tools for deterministic checks

Use tools for facts that should not depend on the LLM’s reasoning. In underwriting, that usually means policy lookup and risk scoring.

from crewai.tools import BaseTool

class PolicyCheckTool(BaseTool):
    name: str = "policy_check"
    description: str = "Checks payment underwriting policy constraints."

    def _run(self, country: str, mcc: str) -> str:
        blocked_countries = {"IR", "KP", "SY"}
        restricted_mccs = {"7995", "6211"}  # example only
        if country in blocked_countries:
            return f"reject: blocked_country:{country}"
        if mcc in restricted_mccs:
            return f"review: restricted_mcc:{mcc}"
        return "pass"

class RiskScoreTool(BaseTool):
    name: str = "risk_score"
    description: str = "Computes a simple underwriting risk score."

    def _run(self, chargeback_rate: float, fraud_score: float) -> str:
        score = (chargeback_rate * 100) + (fraud_score * 50)
        if score >= 80:
            return f"high:{score:.1f}"
        if score >= 40:
            return f"medium:{score:.1f}"
        return f"low:{score:.1f}"

3) Build the CrewAI agent and task

This is the actual pattern you want in production: one agent with narrow scope, explicit tools, and a task that forces structured output. Keep the prompt short and specific.

from crewai import Agent, Task, Crew
from crewai.llm import LLM

llm = LLM(model="gpt-4o-mini")

underwriting_agent = Agent(
    role="Payments Underwriting Analyst",
    goal="Decide whether a merchant should be approved for payment processing.",
    backstory=(
        "You underwrite merchants for card payments. "
        "You must follow policy strictly and prefer human review when uncertain."
    ),
    llm=llm,
    tools=[PolicyCheckTool(), RiskScoreTool()],
    verbose=True,
)

underwriting_task = Task(
    description=(
        "Review the merchant data below and return one of: approve, review, reject.\n"
        "Use the tools for policy and risk evaluation.\n"
        "Merchant data:\n{merchant_json}\n"
        "Return a short decision summary with reason codes."
    ),
    expected_output="A concise underwriting decision with reason codes.",
    agent=underwriting_agent,
)

crew = Crew(
    agents=[underwriting_agent],
    tasks=[underwriting_task],
)

4) Run the crew and persist an audit record

The output should be written alongside the input payload and policy version. That is what makes this usable in payments operations when someone asks why a merchant was declined.

import json
from datetime import datetime

merchant_input = UnderwritingInput(
    merchant_name="Acme Subscriptions",
    country="US",
    mcc="5734",
    monthly_volume_usd=25000,
    avg_ticket_usd=49.99,
    chargeback_rate=0.012,
    fraud_score=0.18,
    kyc_status="pass",
    sanctions_hit=False,
)

result = crew.kickoff(inputs={"merchant_json": merchant_input.model_dump_json()})

audit_record = {
    "timestamp_utc": datetime.utcnow().isoformat(),
    "policy_version": "2026-04-01",
    "input": merchant_input.model_dump(),
    "decision": str(result),
}

with open("underwriting_audit.jsonl", "a", encoding="utf-8") as f:
    f.write(json.dumps(audit_record) + "\n")

print(result)

Production Considerations

  • Keep PII out of prompts where possible

    • Use merchant IDs instead of raw names when you can.
    • If you must include sensitive fields for decisioning, encrypt at rest and redact them from logs.
  • Treat policy as code

    • Version your underwriting rules separately from the agent prompt.
    • Store policy versions with every decision so compliance can reproduce outcomes later.
  • Add human review thresholds

    • Auto-approve low-risk merchants only.
    • Route medium-risk or incomplete KYC cases to manual review instead of forcing an LLM decision.
  • Respect data residency

    • Payments data often cannot leave a region.
    • Pin your model endpoint and storage to approved regions and document every third-party dependency in your DPIA or vendor review.

Common Pitfalls

  • Letting the model decide without hard rules

    • Mistake: asking the agent to “figure it out” from raw context.
    • Fix: run deterministic checks first; let CrewAI explain or combine them.
  • No audit trail

    • Mistake: storing only the final approval or rejection.
    • Fix: persist input snapshot, tool outputs, prompt version, model version, and policy version together.
  • Mixing compliance logic into free-form prompts

    • Mistake: burying sanctions or MCC rules inside natural language instructions.
    • Fix: enforce those checks in tools or pre-processing code so they are testable and consistent.

If you want this to hold up in a real payments stack, keep the agent narrow. Use CrewAI for orchestration and explanation; use Python services for policy enforcement, logging, residency controls, and anything that needs deterministic behavior.


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