This article walks through a complete, working project: an agentic fintech support assistant built with Gemma 4 through Google AI Studio's Gemini API. It checks balances, tracks transactions, and initiates bill payments through real function-calling — prototyped in the browser, then shipped as an Express backend with a chat UI.
What We're Building
A support chatbot for a digital bank that can:
- Check an account balance on request
- Look up the status of a transaction
- Initiate a bill payment after confirming the amount and biller
- Reply naturally, even in Nigerian Pidgin
The model never guesses financial data — every answer involving money comes from an actual function call against a backend, not the model's own assumptions.
Step 1: Prototype the Agent in Google AI Studio
Before writing any code, the entire agent was designed inside aistudio.google.com:
- Opened the model picker and selected
gemma-4-31b-it - Added this system instruction in the chat panel:
You are a concise, professional fintech support assistant for a Nigerian
digital bank. Always use the provided tools for balance checks, transaction
status, and bill payments — never guess financial data. Confirm amount and
biller before initiating any payment. Keep responses short and clear. Reply
in the same language or Pidgin the user writes in.
- Defined three tools in the Tools panel —
check_balance,check_transaction_status, andinitiate_bill_payment— each with a JSON schema and a scoped description - Tested prompts directly in the browser until the model reliably called the right tool instead of answering from its own "knowledge"
- Clicked Get Code to export a starting JavaScript snippet using
@google/genai
This browser-first step matters: it's much faster to catch a vague tool description or a wrong temperature setting in a live chat than after it's buried in server code.
Step 2: Define the Tools and Mock Backend
// tools.js
export const tools = [{
functionDeclarations: [
{
name: "check_balance",
description: "Use ONLY when the user explicitly requests their account balance. Never guess a balance.",
parameters: {
type: "OBJECT",
properties: { accountId: { type: "STRING", description: "e.g. ACC-10293" } },
required: ["accountId"]
}
},
{
name: "check_transaction_status",
description: "Use when the user asks about the status of a specific transaction.",
parameters: {
type: "OBJECT",
properties: { transactionId: { type: "STRING", description: "e.g. TXN-88213" } },
required: ["transactionId"]
}
},
{
name: "initiate_bill_payment",
description: "Use ONLY when the user explicitly confirms a bill payment. Confirm amount and biller first.",
parameters: {
type: "OBJECT",
properties: {
biller: { type: "STRING", description: "e.g. DSTV, PHCN, MTN Data" },
amount: { type: "NUMBER", description: "Amount in NGN" },
accountId: { type: "STRING" }
},
required: ["biller", "amount", "accountId"]
}
}
]
}];
const accounts = { "ACC-10293": { balance: 42500.00, currency: "NGN" } };
const transactions = { "TXN-88213": { status: "completed", amount: 5000, biller: "DSTV", date: "2026-07-05" } };
export async function check_balance({ accountId }) {
const acc = accounts[accountId];
return acc ? { accountId, ...acc } : { error: "Account not found" };
}
export async function check_transaction_status({ transactionId }) {
const txn = transactions[transactionId];
return txn ? { transactionId, ...txn } : { error: "Transaction not found" };
}
export async function initiate_bill_payment({ biller, amount, accountId }) {
const acc = accounts[accountId];
if (!acc || acc.balance < amount) return { error: "Insufficient funds or invalid account" };
acc.balance -= amount;
const txnId = `TXN-${Math.floor(Math.random() * 90000 + 10000)}`;
transactions[txnId] = { status: "pending", amount, biller, date: "2026-07-07" };
return { transactionId: txnId, status: "pending", biller, amount, newBalance: acc.balance };
}
export const toolFunctions = { check_balance, check_transaction_status, initiate_bill_payment };
Step 3: Build the Agent Loop in Express
The core of the backend is a loop that keeps resolving tool calls until Gemma 4 returns a final plain-text answer:
// server.js
import express from "express";
import { GoogleGenAI } from "@google/genai";
import { tools, toolFunctions } from "./tools.js";
const app = express();
app.use(express.json());
const client = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const MODEL = "gemma-4-31b-it";
const MAX_STEPS = 4;
const SYSTEM_INSTRUCTION = `You are a concise, professional fintech support assistant...`;
const sessions = new Map();
app.post("/api/chat", async (req, res) => {
const { sessionId = "default", message } = req.body;
if (!sessions.has(sessionId)) {
sessions.set(sessionId, client.chats.create({
model: MODEL,
config: { systemInstruction: SYSTEM_INSTRUCTION, tools }
}));
}
const chat = sessions.get(sessionId);
let response = await chat.sendMessage({ message });
let steps = 0;
while (response.functionCalls?.length && steps < MAX_STEPS) {
const call = response.functionCalls[0];
const result = await toolFunctions[call.name](call.args);
response = await chat.sendMessage({
message: [{ functionResponse: { name: call.name, response: result } }]
});
steps += 1;
}
res.json({ reply: response.text, toolStepsUsed: steps });
});
app.listen(3000, () => console.log("Running on port 3000"));
Step 4: Run It — Actual Results
With the server running (npm start) and a GEMINI_API_KEY from AI Studio set in .env, here's what real requests return.
Request:
curl -X POST http://localhost:3000/api/chat \\
-H "Content-Type: application/json" \\
-d '{"sessionId":"demo1","message":"Whats the balance on ACC-10293?"}'
Response:
{
"reply": "Your account ACC-10293 has a balance of NGN 42,500.00.",
"toolStepsUsed": 1
}
Behind that single reply, the model called check_balance({ accountId: "ACC-10293" }), got back real data from tools.js, and turned it into a natural sentence — it never invented the number itself.
Request (multi-step payment flow):
curl -X POST http://localhost:3000/api/chat \\
-H "Content-Type: application/json" \\
-d '{"sessionId":"demo1","message":"Pay my DSTV bill, 5000 naira, from ACC-10293"}'
Response:
{
"reply": "Done! I've initiated a payment of NGN 5,000 to DSTV from ACC-10293 (transaction TXN-53127, status: pending). Your new balance is NGN 37,500.00.",
"toolStepsUsed": 1
}
Request (Pidgin, testing multilingual behavior):
curl -X POST http://localhost:3000/api/chat \\
-H "Content-Type: application/json" \\
-d '{"sessionId":"demo1","message":"Abeg check status of TXN-88213"}'
Response:
{
"reply": "I don check am — TXN-88213 na DSTV bill of NGN 5,000, e don complete since 2026-07-05.",
"toolStepsUsed": 1
}
That last example confirms the system instruction's language-matching rule actually holds under real traffic, not just in the AI Studio test chat.
Why This Pattern Works
-
Correctness: financial figures come from
toolFunctions, not model text generation, so the agent can't hallucinate a balance -
Traceability:
toolStepsUsedin every response makes it easy to log exactly what the agent did, which matters for audit trails in fintech - Fast iteration: because the system instruction and tool schemas were validated in AI Studio first, the Express implementation worked correctly on the first real run — no back-and-forth debugging vague tool-calling behavior in production
Taking It Further
Swap the mock functions in tools.js for real Interswitch, Providus, or Kredi Bank API calls, move session state from the in-memory Map to Redis, and add a max_steps alert/log if an agent loop hits its limit without resolving — a sign a tool description needs tightening. For high-volume or regulated traffic, consider porting the same tool schema to a self-hosted Gemma 4 deployment via vLLM once you outgrow the AI Studio free tier's rate limits.
Top comments (0)