Inspiration

Consumer lenders and regulated wallets in Latin America get thousands of complaints a month about the account and its operations: "I paid and it does not show", a charge the customer does not recognise, a duplicate debit, a transfer that never arrived, a payoff certificate that never came. In our reading of the Argentine rules (our reading, not legal advice), every complaint needs a number, a written answer within 10 business days and an auditable register. Customers write on WhatsApp, in their own words, often with a photo of a receipt.

Today a support team copies the message into a ticketing tool, asks for the receipt again, waits for the collection network's settlement file, checks it by hand and writes the answer days later. Meanwhile the same WhatsApp thread is where someone may be tempted to remind the customer of an overdue instalment, which WhatsApp's commerce policy does not allow and the regulator frowns upon.

We wanted an agent that does the investigation work in minutes, inside the rules, with the code (not the model) deciding amounts, caps, deadlines and what may be sent on each channel, and with a person approving whatever goes beyond the agent's cap.

What it does

Reclamo en regla is a regulated-complaints agent for consumer lenders and wallets:

  • Takes any account complaint on WhatsApp (AWS End User Messaging Social): payments, charges, duplicate debits, transfers, disbursements, payoff certificates, account closure, data changes, blocked accounts and suspected fraud.
  • Gives a complaint number on the spot. An input classifier decides whether the message is a complaint, and the code files it with its number (R-2026-000123) and its regulatory deadline in the same transaction that writes its first register entry, before the agent's turn even starts.
  • Checks identity by code before any data. The linked phone (hashed) plus a challenge verified by code. The agent never asks for documents, passwords, PINs, cards or bank details.
  • Investigates against the lender's systems: reads the receipt image, reconciles it with the collection network's settlement file, checks a charge against the fee schedule, looks for the duplicate debit, asks the transfer network for the trace.
  • Resolves what the policy allows, exactly once: applies the payment, reverses a wrong charge with a credit note, refunds a duplicate debit, requests a transfer reversal, issues the payoff certificate. Every money movement takes a claim on its natural key. Above the agent's cap it asks for an approval that only a person can give in the console, after a recent sign-in.
  • Answers in writing by email (Amazon SES) with the PDF answer and certificates; SES delivery events become the delivery proof recorded in the register.
  • Hands everything else to a person with the regulatory clock running: reminders and escalations are EventBridge Scheduler timers; answering in writing cancels the remaining milestones.
  • Blocks debt collection on WhatsApp for every sender. One outbound gate checks every message, from the agent, a human operator or the system. A collection reminder on WhatsApp is blocked and rerouted to an allowed path (a formal notice by email or the human queue), even when a human operator types it.
  • Keeps an append-only register and compliance tape, chained by hash and anchored outside the tables, so an overwrite is detected, not just forbidden.

What is real and what is simulated

Real in the deployed stage Simulated Data
Amazon Bedrock AgentCore (Harness, Gateway, Policy, Memory), Bedrock Guardrails, the input classifier and the multimodal receipt reader on Bedrock, the WhatsApp channel on AWS End User Messaging Social (SendWhatsAppMessage, UTILITY templates, media through GetWhatsAppMessageMedia, inbound and status events through Amazon SNS), Amazon SES end to end with configuration-set events (recipients are the SES mailbox simulator), EventBridge Scheduler, SQS FIFO, Lambda, DynamoDB, S3 with GuardDuty Malware Protection, Cognito (sign-up and recovery emails through SES), CloudFront and WAF, CloudWatch Logs anchors. For anyone testing without a phone, the console includes a phone simulator that enters through the same inbound Lambda, identity check, limits, classifier and gate. The lender's core, the collection networks and the transfer network (mocks behind IAM-authenticated Function URLs and OpenAPI 3.1 contracts); a paused clock per world 100% synthetic; the lender, its customers and its networks are fictitious

How we built it

  • AWS Communication Developer Services:
    • Amazon SES v2 (sending only): the acknowledgement, the formal written answer, payoff certificates, credit notes and formal debt notices go out as MIME emails with PDF attachments (SendEmail with Content.Raw, one fenced recipient, a configuration set with TLS required and no tracking). Delivery, bounce, complaint and reject events flow through Amazon EventBridge to a Lambda that turns them into message statuses and into the delivery proof of each written answer in the register. The domain identity signs with Easy DKIM and publishes DMARC p=reject. Cognito's emails and the lead notice to Craftech also go through SES.
    • AWS End User Messaging Social (WhatsApp): SendWhatsAppMessage for free text inside the 24-hour window, interactive reply buttons and lists, and four UTILITY templates kept as code; inbound messages, media and statuses arrive through Amazon SNS (signature verified); GetWhatsAppMessageMedia copies a receipt image to S3, where GuardDuty scans it before it is read. Templates and the event destination are created by scripts. The channel mode is per world, so guest and test worlds can never go live.
  • The agent: Amazon Bedrock AgentCore Harness running Claude Sonnet 5.5 through a tagged application inference profile, one turn per customer event, invoked by a worker from an SQS FIFO queue ordered per customer. Before the model call, the worker runs in code the reads every turn needs (customer, account summary, last operations, open complaints) and passes them as escaped tool results, so the model spends its calls on the investigation. Every inbound message gets an answer.
  • Tools: an AgentCore Gateway (MCP, IAM auth) with 7 Lambda targets and 28 tools: identity, account, investigation, actions, complaints, messaging and handoff. Every tool validates with a strict zod schema and takes its ids from a signed, short-lived session token, never from the model. AgentCore Policy (Cedar, ENFORCE) authorizes every call; there is no tool that approves.
  • Memory and guardrails: AgentCore Memory keeps the customer's preferences and summaries with an exclusion instruction for sensitive data. Guardrail G1 filters input (prompt attacks, denied topics, masking); G2 checks every outbound text for contextual grounding on the turn's tool results; a deterministic verifier checks that every amount, date and trace in a message came from a tool.
  • Two smaller models outside the loop: an input classifier on Claude Haiku with forced tool output, so the complaint number never waits for the agent, and a multimodal receipt reader on Bedrock Converse with schema-forced output and a deterministic fallback parser. A reading alone never applies a payment: the settlement line does.
  • Obligations after each turn: code checks what the turn had to do (acknowledge, answer in writing, reconcile when the file is published) and queues a follow-up turn if it was not done.
  • Time: every milestone is an EventBridge Scheduler schedule; each world has its own paused clock, so a 10-business-day story plays in minutes. Abuse limits always use the real clock.
  • Stack: SST v4, serverless only, all infrastructure as code, one stage deployed only by GitHub Actions with OIDC. TypeScript monorepo, React 19 + Vite + Tailwind v4 console with a guided tour, tRPC v11 BFF on Lambda, Cognito with our own sign-up and sign-in. 128 documented flows, each traced to its tests: about 3,390 unit and local-flow tests, Playwright UI tests, a scenario runner against the deployed stage, and 100 agent tests on a local model before anything reaches Bedrock.

Pre-existing work disclosure. The repository skeleton (SST and CI setup, CI bootstrap template, lint scripts, console shell and shared components, our own Cognito sign-in, tRPC base, logging, retry and crypto helpers, the PDF generator, the landing gallery, AgentCore setup patterns, agent definitions in .claude/) was adapted from internal Craftech scaffolding built before the contest period. Process skills in .claude/skills/ come from mattpocock/skills (development tooling, not product code); the diagram's AWS Architecture Icons come from the MIT-licensed aws-icons package. The product, its domain, its channel policy, its register, its tools and its data were built during the Submission Period.

Challenges we ran into

  • An agent that answered the identity challenge for the customer. Our first local agent test caught it: the customer wrote "hola", and the model called the challenge-verification tool with the example date printed in the challenge's own prompt, which happened to be right. Identity is now fenced in code: the tool only accepts an answer the customer actually wrote in that conversation, and every agent test checks it.
  • Collection rules that also caught the lender's own terms. The lexicon that blocks collection on WhatsApp also blocked neutral answers ("the instalment is overdue") and quotes from the lender's terms. We split the categories, taught the gate to name the rule it applied so the agent can rewrite once, and kept zero false negatives on the collection corpus.
  • "Always answer" broken by our own policy. When a customer with no open complaint asked for a person, the handoff notice was treated as a proactive WhatsApp and blocked, so the customer got no reply. The gate now links every notice to the message it answers, and the channel policy still checks the thread and the 24-hour window.
  • Grounding numbers without blocking good answers. The verifier rejected the complaint's own number and deadline as "figures no tool returned". It now grounds the complaint's facts and what the case file proved, never another payer's line or a failed reading.
  • An append-only register on DynamoDB. IAM can block updates, deletes and batch writes, but not an overwrite with PutItem. We detect it instead: a hash chain, anchors written only by one Lambda to CloudWatch Logs, and hashes printed in every delivered PDF. We never claim more than that.
  • Testing a non-deterministic agent without burning tokens. A full suite on the production model costs real money per run, so the agent tests run on a local model with the same prompt, tools, policy and gate; only the model differs.

Accomplishments that we're proud of

  • A complaint number on the spot, before the agent's turn, in the same transaction as its register entry.
  • A channel-policy gate that stops people too: a human operator who types a collection reminder on WhatsApp is blocked and offered a formal email notice instead, and the tape shows why.
  • Money moves exactly once: every payment, reversal and refund is claimed on its natural key, and an approved action is executed by the worker re-reading the approval, never from an event payload.
  • A guided tour that a visitor can run alone: sign up, get an isolated world with its own lender and clock, and follow Valeria's "I paid and it does not show" from WhatsApp to the delivered written answer in five minutes.
  • 100 agent tests on a local model, at no token cost, that found real bugs (including the identity one) before any customer could.

What we learned

  • The model is good at investigating and drafting; amounts, caps, deadlines, identity and channel rules are safer in code around it.
  • A guardrail that blocks without saying why makes an agent loop. Naming the rule and allowing one rewrite turns a block into a better answer.
  • Testing an agent is testing everything around it: the most valuable findings of the local runs were bugs in the code the agent depends on, not in the prompt.
  • A local model checks decisions (which tools, in what order, what it must not do); it does not sign off the wording of the production model.

What's next for Reclamo en regla

  • A pilot with a consumer lender or wallet on its own WhatsApp Business number, plugging its core and its collection networks behind the same OpenAPI contracts that the mocks implement today.
  • Regulatory profiles for more Latin American countries (deadlines, register format, channel rules) as configuration, not prompt.
  • A capped run of the full scenario suite on the production model, with per-step pass rates published.

Built With

Share this project:

Updates

Submission history