DEV Community

Cover image for Building a Full Gemma 4 + Google AI Studio Project — A Fintech Support Agent
Agbo, Daniel Onuoha
Agbo, Daniel Onuoha

Posted on

Building a Full Gemma 4 + Google AI Studio Project — A Fintech Support Agent

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:

  1. Opened the model picker and selected gemma-4-31b-it
  2. 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.
Enter fullscreen mode Exit fullscreen mode
  1. Defined three tools in the Tools panel — check_balance, check_transaction_status, and initiate_bill_payment — each with a JSON schema and a scoped description
  2. Tested prompts directly in the browser until the model reliably called the right tool instead of answering from its own "knowledge"
  3. 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 };
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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?"}'
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "reply": "Your account ACC-10293 has a balance of NGN 42,500.00.",
  "toolStepsUsed": 1
}
Enter fullscreen mode Exit fullscreen mode

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"}'
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"}'
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "reply": "I don check am — TXN-88213 na DSTV bill of NGN 5,000, e don complete since 2026-07-05.",
  "toolStepsUsed": 1
}
Enter fullscreen mode Exit fullscreen mode

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: toolStepsUsed in 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)