Spend Narrative Agent · AfifAI
Case study · AI engineering · 2026

A Teams agent that turns card spend into a donor update

Nonprofit finance staff ask in Teams or Microsoft 365 Copilot: “what did we spend last quarter?”, “who holds cards?”, “write a donor follow-up for Q1”. The agent pulls their own card data, does the arithmetic in code, and writes a table or a three-paragraph donor email using only those figures.

TypeScriptBot Framework (CloudAdapter)Azure Bot ServiceAzure OpenAI gpt-4oModel Context ProtocolAdaptive CardsApp Service
Client
US prepaid-card & spend-management fintech serving nonprofits (confidential)
My role
Team lead & architect (hands-on)
Stage
Working agent on Azure, sideloaded in Teams & Copilot, May 2026
Status
Proof of concept
At a glance
≈60 to <1 minto produce a donor spend update, from export, tagging and writing by hand to one question (estimate)
3 to 1round-trips for a spend summary: profile, tags and transactions fetched in parallel
501 to 200response to Copilot invoke activities, which had marked the agent as broken
2data paths, the REST API and the platform’s MCP server, switched by one setting
0figures left to the model. Totals, categories and top spends are computed in code
6intents, classified in JSON mode at temperature 0
7deploy iterations in 3 days, from first build to a working Teams package

“Estimate” numbers are engineering estimates to confirm with users. All others are counted from the code and the deployment artefacts.

The problem

The data was in the card platform. The story for donors was not.

Nonprofits that run program spending on prepaid cards owe their donors an account of where the money went. Producing one meant exporting transactions, mapping each to a category, adding up totals and then writing prose that a finance-literate donor would trust.

The card provider wanted that available where staff already work, in Teams and Copilot, without the provider holding a shared credential to every customer’s account, and without a language model “estimating” a single number.

What I owned

The agent, end to end

  • Designed the agent: the turn flow, the token model, the intent set, and the split between code and model.
  • Wrote most of the TypeScript: the Bot Framework host, the REST and MCP data clients, the period parser, the aggregator and the prompts.
  • Diagnosed the Copilot-specific failures (invoke activities, session affinity) from App Service logs and fixed them.
  • Packaged and deployed it: App Service, Azure Bot Service, and the Teams manifest with a custom engine agent.
What I built

A small agent with a strict division of labour

Architecture

Code computes, the model writes

The diagram is wider than the screen: swipe sideways to see all of it →

The model receives aggregates and a flattened transaction list, never raw API payloads to add up, and is told never to invent or estimate a figure. An empty date range is answered by code without calling the model at all.

Design decisions

Keep the arithmetic away from the model

Deterministic aggregation

Totals, category sums, top purchases and “untagged” counts are computed in TypeScript and handed to the model as data. A donor email can be wrong in tone, but not in its numbers. Trade-off: new kinds of analysis need code, not just a prompt change.

Each user brings their own token

No shared service credential to customer accounts. The token is entered in a masked field and validated before use, so a bad token fails at connect time, not halfway through a question. Trade-off: for this pilot the token lives in server memory, and moving it to a secure store is the first production item.

Two data paths, one switch

The REST API was ready. The provider’s MCP server was the strategic direction. Both paths sit behind the same functions and one setting chooses between them. Trade-off: two response shapes to normalise.

Answer shape follows the request

“List” gets a markdown table sorted by date. “Summary” or “donor follow-up” gets exactly three paragraphs with no bullets, ready to paste into an email. Factual questions get one plain answer.

Engineering log

What broke, and how I found out

Most of the issues only appeared inside Teams and Copilot, not in local tests. A selection:

Copilot

Copilot marked the agent as broken

Copilot and Teams send invoke activities alongside messages. With no handler, the adapter answered 501 Not Implemented, and Copilot treated the agent as faulty.

Explicit 200 invokeResponse for invoke activities, silent acknowledgement for events, and an error handler that never throws on non-message turns.

MCP

The MCP session vanished between calls

The MCP server runs behind a load balancer. Every call after initialize landed on another instance and lost its session.

Capture every Set-Cookie (including the affinity cookie) and send them back on each call. Parse both SSE “data:” frames and plain JSON replies.

MCP

Which tool returns transactions?

The transaction tool’s name was not settled on the server side, and a wrong name came back as a tool error, not an exception.

An ordered fallback over four candidate tool names that stops at the first success and returns an empty list otherwise, so a summary never crashes.

Data quality

Untagged spend skewed the categories

Some transactions had no tag, so category totals silently disagreed with the overall total.

Untagged spend falls back to the platform category, then to the merchant name, and the count is passed to the model as a warning that it must mention.

Dates

“Summarize our spend” had no period

Left to the model, an unspecified period becomes a guess presented as fact.

A deterministic parser for common periods. If the message names no period, the agent asks a clarifying question instead of calling the API.

Context

“And last quarter?”

Follow-ups made no sense without the previous turn.

The classifier rewrites each message to stand alone, using the last 10 turns, before it is routed.

Try it

Turns, from token to donor email

An illustrative replay with synthetic amounts and merchants. It contains no client data or code. Each scenario shows what the agent does before it answers.

Outcome

Where it landed

MeasureBeforeAfterSource
Donor spend updateExport, tag, sum and write, ≈60 minOne question, under 1 minestimate
Round-trips per summary3 sequential1 parallelderived
Copilot invoke handling501, agent marked broken200 acknowledgedcode
Figures computed by the modelPossible0, all in codecode
Bad credentials detectedMid-questionAt connect, with a fix-it messagecode
Deploy iterations to a working package—7 in 3 daysartefacts

Source: code = in the repository; artefacts = timestamps of the deployment packages; derived = follows from the code; estimate = to confirm with users.

  • A working agent on App Service and Azure Bot Service, sideloaded in Teams and Microsoft 365 Copilot (Teams package v1.0.3).
  • Six question types: profile, tags, cardholders, spend summary, transaction list, help.
  • Both data paths working, the REST API and the MCP server.
  • Donor-ready three-paragraph narratives and date-sorted tables from the same data.
  • Next steps: move per-user tokens and history to a secure shared store, add balance queries once the API exposes them, and add automated tests around the aggregator and period parser.
Stack

Stack

Agent
TypeScript, Node.js, Bot Framework CloudAdapter, Express
AI
Azure OpenAI gpt-4o (JSON-mode classifier, grounded writer)
Data
Spend platform REST API, Model Context Protocol (JSON-RPC, SSE)
Channels
Azure Bot Service, Microsoft Teams, Microsoft 365 Copilot (custom engine agent)
UI
Adaptive Cards (masked token input)
Hosting
Azure App Service, Teams manifest 1.22

Free scoping call · 30 minutes · No commitment

Bring one process. Leave with a plan and a number to beat.

Write a few lines: the process or system, what hurts today (time, errors, cost or an AI pilot that stalled) and who uses it. After a short call you get a written plan and a fixed quote within ≈3 days.

Get a free scoping callafif.mansour@gmail.com

Opens a pre-filled email · reply within one business day

The model reads. The code counts.

All projects