Bonaventure OgetoBy Bonaventure Ogeto|

Building an AI Agent That Queries Your Paystack Transactions

Build a read-only Paystack query agent: 1) Define tools: list_transactions (GET /transaction with filters), get_transaction (GET /transaction/:id), search_by_email (GET /transaction?customer=email). 2) Pass tools to Claude API. 3) When Claude calls a tool, execute it server-side using PAYSTACK_SECRET_KEY from env — the key never reaches the LLM. 4) Return tool results to the LLM for synthesis. Keep the agent strictly read-only. Do not include transfer, refund, or charge tools until you have safety rails in place.

Building the Query Agent

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

// Tool definitions — the LLM sees these, not the key
const tools = [
  {
    name: 'list_transactions',
    description: 'List Paystack transactions with optional filters. Returns array of transaction objects.',
    input_schema: {
      type: 'object',
      properties: {
        status: { type: 'string', enum: ['success', 'failed', 'pending'] },
        from: { type: 'string', description: 'ISO date string, e.g. 2026-07-01' },
        to: { type: 'string', description: 'ISO date string' },
        perPage: { type: 'number', description: 'Results per page, max 100' },
      },
    },
  },
  {
    name: 'get_transaction',
    description: 'Get a single Paystack transaction by ID.',
    input_schema: {
      type: 'object',
      properties: { id: { type: 'number' } },
      required: ['id'],
    },
  },
  {
    name: 'search_by_email',
    description: 'Find all transactions from a specific customer email.',
    input_schema: {
      type: 'object',
      properties: { email: { type: 'string' } },
      required: ['email'],
    },
  },
];

// Server-side execution — key never leaves this function
async function executeTool(toolName, toolInput) {
  var headers = { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY };
  if (toolName === 'list_transactions') {
    var params = new URLSearchParams(toolInput).toString();
    var res = await fetch('https://api.paystack.co/transaction?' + params, { headers });
    return (await res.json()).data;
  }
  if (toolName === 'get_transaction') {
    var res = await fetch('https://api.paystack.co/transaction/' + toolInput.id, { headers });
    return (await res.json()).data;
  }
  if (toolName === 'search_by_email') {
    var res = await fetch('https://api.paystack.co/transaction?customer=' + toolInput.email, { headers });
    return (await res.json()).data;
  }
}

// Agent loop
async function runPaystackAgent(userQuestion) {
  var messages = [{ role: 'user', content: userQuestion }];

  while (true) {
    var response = await client.messages.create({
      model: 'claude-opus-4-6',
      max_tokens: 1024,
      tools,
      messages,
    });

    if (response.stop_reason === 'end_turn') {
      return response.content.find(b => b.type === 'text')?.text;
    }

    if (response.stop_reason === 'tool_use') {
      // Execute all tool calls
      var toolResults = [];
      for (var block of response.content) {
        if (block.type === 'tool_use') {
          var result = await executeTool(block.name, block.input);
          toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result) });
        }
      }
      messages.push({ role: 'assistant', content: response.content });
      messages.push({ role: 'user', content: toolResults });
    }
  }
}

Example usage:

var answer = await runPaystackAgent('How much revenue did we collect last week, broken down by success vs failed?');
console.log(answer);
// → "Last week you processed NGN 1,247,500 in successful transactions (47 payments) and NGN 83,200 in failed attempts (12 payments)."

Learn More

See why you must never give the LLM your secret key for the security reasoning behind this pattern.

Sign up for the McTaba newsletter

Key Takeaways

  • Define read-only tools for the LLM — list, get, search. No write tools until you have safety rails.
  • The LLM calls the tool; your server executes it using the secret key from environment variables.
  • The key never appears in the LLM prompt or response — only tool call names and results.
  • Claude function calling returns a structured tool_call object — match on tool name and execute the right handler.
  • Rate limit your agent's tool calls to avoid burning through Paystack API rate limits.

Frequently Asked Questions

What Paystack data can I safely give to the LLM in tool responses?
Transaction IDs, amounts, dates, status, channel (card/mobile money), and reference codes are safe to return to the LLM. Be careful with full card numbers (never return these — Paystack masks them anyway) and customer email addresses (return only what you need for the task). Avoid returning authorization_code in contexts where the LLM output might be logged or displayed.
How do I handle Paystack rate limits in an agent loop?
Paystack enforces rate limits per API key. An agent that runs 10 tool calls per user message can exhaust rate limits quickly. Add a request counter in your executeTool function and throw if it exceeds a per-session cap (e.g., 20 API calls per agent conversation). Also cache list_transactions results within a session — if the LLM calls the same endpoint twice, return the cached result.
Can I use this pattern with GPT-4 or Gemini instead of Claude?
Yes — OpenAI and Google also support function calling with the same server-side execution pattern. The tool definitions differ slightly in schema format (OpenAI uses "function" wrappers, Google uses "functionDeclarations"), but the principle is identical: the LLM sees tool names and results, your server holds the key and executes the calls.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon