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

By Cyprian AaronsUpdated 2026-04-21
underwritingautogenpythonpayments

An underwriting agent for payments evaluates a merchant or transaction against policy, risk, and compliance rules before you approve processing. In practice, it helps you reduce fraud exposure, enforce KYC/AML checks, and keep approval decisions auditable instead of buried in a human inbox.

Architecture

  • Risk intake service
    • Receives merchant profile, transaction metadata, MCC, geography, chargeback history, and KYC status.
  • Policy engine
    • Encodes underwriting rules like velocity thresholds, prohibited industries, sanctions screening results, and reserve requirements.
  • AutoGen agent layer
    • Coordinates the underwriting workflow with AssistantAgent and UserProxyAgent, and calls tools for scoring and document checks.
  • Tooling layer
    • Exposes deterministic Python functions for risk scoring, policy lookup, and audit logging.
  • Decision store
    • Persists approvals, declines, manual review flags, and the exact inputs used for the decision.
  • Audit and monitoring
    • Captures prompts, tool outputs, decision reasons, latency, and exception paths for compliance review.

Implementation

1) Install AutoGen and define your underwriting tools

Use real Python functions for anything that must be deterministic. The model should reason over results; it should not invent scores or policy outcomes.

from typing import Dict, Any
import json
import datetime

def calculate_risk_score(merchant: Dict[str, Any]) -> Dict[str, Any]:
    score = 0

    if merchant.get("country") in {"IR", "KP", "SY"}:
        score += 80
    if merchant.get("mcc") in {"4829", "6012"}:
        score += 25
    if merchant.get("chargeback_rate", 0) > 0.9:
        score += 30
    if merchant.get("kyc_status") != "verified":
        score += 20

    return {
        "risk_score": min(score, 100),
        "timestamp": datetime.datetime.utcnow().isoformat()
    }

def check_policy(merchant: Dict[str, Any]) -> Dict[str, Any]:
    if merchant.get("country") in {"IR", "KP", "SY"}:
        return {"decision": "decline", "reason": "sanctions_restricted_country"}
    if merchant.get("chargeback_rate", 0) > 1.5:
        return {"decision": "manual_review", "reason": "high_chargeback_rate"}
    if merchant.get("kyc_status") != "verified":
        return {"decision": "manual_review", "reason": "kyc_incomplete"}
    return {"decision": "approve", "reason": "policy_passed"}

def write_audit_log(event: Dict[str, Any]) -> str:
    with open("underwriting_audit.jsonl", "a") as f:
        f.write(json.dumps(event) + "\n")
    return "ok"

2) Create an AutoGen assistant that can call those tools

For payments underwriting, keep the agent narrow. It should classify risk and explain the decision; it should not free-form negotiate policy.

import autogen

llm_config = {
    "config_list": [
        {
            "model": "gpt-4o-mini",
            "api_key": "${OPENAI_API_KEY}"
        }
    ],
    "temperature": 0
}

assistant = autogen.AssistantAgent(
    name="underwriting_assistant",
    llm_config=llm_config,
    system_message=(
        "You are a payments underwriting agent. "
        "Use only provided tool outputs to make decisions. "
        "Return one of: approve, decline, manual_review. "
        "Always include a concise reason suitable for audit."
    )
)

user_proxy = autogen.UserProxyAgent(
    name="underwriting_controller",
    human_input_mode="NEVER",
    max_consecutive_auto_reply=3,
)

3) Register tools and run the workflow

This is the actual pattern: the controller sends the case data to the assistant, the assistant reasons over tool outputs, then you persist the decision.

def underwrite_case(case: Dict[str, Any]) -> Dict[str, Any]:
    risk = calculate_risk_score(case)
    policy = check_policy(case)

    user_proxy.register_function(
        function_map={
            "calculate_risk_score": calculate_risk_score,
            "check_policy": check_policy,
            "write_audit_log": write_audit_log,
        }
    )

    prompt = f"""
Underwrite this payment merchant case.

Case:
{json.dumps(case)}

Risk score:
{json.dumps(risk)}

Policy result:
{json.dumps(policy)}

Return a JSON object with keys:
decision, reason, risk_score
"""

    result = user_proxy.initiate_chat(
        assistant,
        message=prompt,
        clear_history=True
    )

    final_text = result.chat_history[-1]["content"]
    audit_event = {
        "case_id": case["merchant_id"],
        "input": case,
        "risk": risk,
        "policy": policy,
        "agent_output": final_text
    }
    write_audit_log(audit_event)

    return {
        "risk": risk,
        "policy": policy,
        "agent_output": final_text
    }

4) Call it with a payment-specific case

Use fields your payments team actually cares about: country of incorporation, processing volume, chargebacks per month, MCC, and KYC state.

case = {
    "merchant_id": "m_10293",
    "country": "US",
    "mcc": "5812",
    "monthly_volume_usd": 120000,
    "chargeback_rate": 0.4,
    "kyc_status": "verified"
}

result = underwrite_case(case)
print(result)

Production Considerations

  • Keep regulated decisions deterministic
    • Use AutoGen for orchestration and explanation. Keep approval/decline thresholds in code or config so audit teams can reproduce outcomes.
  • Log everything needed for dispute handling
    • Persist input payloads, tool outputs, model version, prompt versioning, timestamps, and final decision text.
  • Respect data residency
    • Route EU merchant data to EU-hosted infrastructure where required. Do not ship PII or bank account data into an unconstrained model endpoint.
  • Add guardrails around adverse actions
    • If the agent recommends decline or reserve placement based on sensitive attributes or restricted signals, require a policy engine confirmation before execution.

Common Pitfalls

  • Letting the model invent policy
    • Bad pattern: asking the LLM to “decide based on intuition.” Fix it by making policy checks explicit functions like check_policy() and treating the model as an interpreter of those results.
  • Skipping auditability
    • If you only store the final answer, you cannot defend the decision later. Log the full input state plus tool outputs and model response in append-only storage.
  • Mixing PII with broad model access
    • Passing raw account numbers or identity documents into every prompt increases exposure. Redact fields first and send only what is needed for underwriting.
  • No manual review path
    • Payments underwriting needs escalation lanes. Anything ambiguous — incomplete KYC, borderline chargebacks under new merchants — should route to manual_review, not forced approval.

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