Currency API for AI Agents: Live Exchange Rates via Tool Calling

Reviewed by Madhushan, Fintech Developer — November 2026

Ask a language model what 2,500 US dollars is in euros and it will answer with confidence. The number will be wrong, because the model last saw a rate during training. The fix is a currency API exposed as a tool. The model decides when it needs a rate, your code fetches it, and the model reports what came back. This tutorial builds that with tool calling in Python, then shows how to skip the code with an MCP server.

Why an agent must never guess a rate

So the rule for the agent is simple: no rate without a tool call, and every rate quoted with its timestamp.

The tool schema

One tool covers most questions. It takes a source currency, a target currency and an optional amount:

{
  "name": "get_exchange_rate",
  "description": "Get the live mid-market exchange rate between two currencies, refreshed every 60 seconds. Optionally convert an amount. Always use this tool for any exchange rate or conversion; never state a rate from memory.",
  "input_schema": {
    "type": "object",
    "properties": {
      "source": { "type": "string", "description": "ISO 4217 code to convert from, e.g. USD" },
      "target": { "type": "string", "description": "ISO 4217 code to convert to, e.g. EUR" },
      "amount": { "type": "number", "description": "Optional amount in the source currency" }
    },
    "required": ["source", "target"]
  }
}

The description does real work. It tells the model when to call the tool and forbids the fallback you are trying to remove.

The implementation

The function calls GET /api/v1/rates with a Bearer key. The response is a JSON array even for one pair, so read [0]. The conversion happens here, not in the model:

import os, re, requests

ALLRATES_KEY = os.environ["ALLRATES_API_KEY"]
ISO = re.compile(r"^[A-Z]{3}$")

def get_exchange_rate(source, target, amount=None):
    source, target = source.upper(), target.upper()
    if not (ISO.match(source) and ISO.match(target)):
        return {"error": "source and target must be 3-letter ISO 4217 codes"}
    r = requests.get(
        "https://allratestoday.com/api/v1/rates",
        params={"source": source, "target": target},
        headers={"Authorization": f"Bearer {ALLRATES_KEY}"},
        timeout=10,
    )
    if r.status_code == 401:
        return {"error": "the exchange rate service rejected the API key"}
    if r.status_code == 429:
        return {"error": "rate limit reached; try again later"}
    r.raise_for_status()
    row = r.json()[0]
    result = {
        "source": row["source"],
        "target": row["target"],
        "rate": row["rate"],
        "rate_type": "mid-market",
        "as_of": row["time"],
    }
    if amount is not None:
        result["amount"] = amount
        result["converted"] = round(amount * row["rate"], 6)
    return result

Errors come back as data, not exceptions. The model can then tell the user the rate is unavailable instead of inventing one. On the Medium plan and above you can pass &amount= to the same endpoint and let the API convert; the local multiplication above works on every plan.

Wiring it into Claude with tool use

With the Anthropic Messages API, you pass the tool list, run the function whenever the response stops with tool_use, and send the result back as a tool_result block. The loop ends when the model answers in text.

import json, anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY

TOOLS = [{
    "name": "get_exchange_rate",
    "description": ("Get the live mid-market exchange rate between two currencies, "
                    "refreshed every 60 seconds. Optionally convert an amount. Always use "
                    "this tool for any exchange rate or conversion; never state a rate from memory."),
    "input_schema": {
        "type": "object",
        "properties": {
            "source": {"type": "string", "description": "ISO 4217 code to convert from, e.g. USD"},
            "target": {"type": "string", "description": "ISO 4217 code to convert to, e.g. EUR"},
            "amount": {"type": "number", "description": "Optional amount in the source currency"},
        },
        "required": ["source", "target"],
    },
}]

SYSTEM = ("You answer currency questions. Never state an exchange rate you did not get "
          "from get_exchange_rate. Quote the as_of time with every rate.")

def ask(question):
    messages = [{"role": "user", "content": question}]
    while True:
        resp = client.messages.create(
            model="claude-sonnet-5-5",
            max_tokens=1024,
            system=SYSTEM,
            tools=TOOLS,
            messages=messages,
        )
        if resp.stop_reason != "tool_use":
            return "".join(b.text for b in resp.content if b.type == "text")
        messages.append({"role": "assistant", "content": resp.content})
        results = []
        for block in resp.content:
            if block.type == "tool_use":
                out = get_exchange_rate(**block.input)
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(out),
                    "is_error": "error" in out,
                })
        messages.append({"role": "user", "content": results})

print(ask("How much is 2,500 USD in EUR and in JPY right now?"))

For that question the model usually makes two tool calls in one turn, one per target. The loop handles both and returns a single answer with both amounts and their timestamps.

The same schema with other providers

The input_schema object is ordinary JSON Schema. OpenAI's function calling takes the same object under parameters in a function definition, and Gemini takes it in a function declaration. The Python function stays exactly as it is. Only the code that reads tool calls from the response and sends results back changes, and every provider's SDK documents that loop.

Production details

Add an official-rate tool for finance questions

A live market rate is the wrong answer to "what rate do I use for this VAT invoice". That question has an official answer: the table of the central bank or tax authority the rule names. A second tool handles it:

def get_official_rate(bank, source, target):
    r = requests.get(
        f"https://allratestoday.com/api/v1/central-bank/{bank}/latest",
        params={"source": source.upper(), "target": target.upper()},
        headers={"Authorization": f"Bearer {ALLRATES_KEY}"},
        timeout=10,
    )
    r.raise_for_status()
    d = r.json()
    return {"bank": d["bank"], "rate": d["rate"], "rate_date": d["rate_date"],
            "rate_type": "official"}

Describe it as "the official rate issued by a named central bank, for invoices, tax and accounting", and the model will choose between the two tools by intent. The rate_date is the institution's own date, so the agent can cite it. Coverage is 123 central banks and 6 tax authorities; the central bank rates API lists the bank codes.

Skip the code: use the MCP server

If your agent runs in a client that supports the Model Context Protocol, such as Claude Desktop, Claude Code, Cursor or VS Code, you do not need to write the tool at all. The AllRatesToday MCP server exposes get_exchange_rate, get_historical_rates, get_official_rates, list_currencies and more.

Use the hosted server, configured with your API key in the Authorization header:

claude mcp add --transport http allratestoday https://allratestoday.com/api/mcp \
  --header "Authorization: Bearer YOUR_API_KEY"

Or run the npm package locally over stdio:

claude mcp add allratestoday -e ALLRATES_API_KEY=YOUR_API_KEY -- npx -y @allratestoday/mcp-server

Setup for Cursor, VS Code, Codex CLI and Gemini CLI is on the MCP page. Agent frameworks that speak MCP can attach the same hosted URL, which is a quick way to test a tool design before you write your own.

Which route to pick

SituationUse
Your own backend calls an LLM APIFunction tool, as above
You need custom rules, caching or logging around each callFunction tool
A desktop or IDE agent, or a framework with MCP supportMCP server
Several clients should share one setupHosted MCP URL

Either way, get a key on the register page, read the API docs, and if you are still comparing providers, start with the free currency API comparison.

FAQ

Can an LLM know today's exchange rate without a tool?

No. A model only knows the rates in its training data, which are months old, and it will still produce a precise-looking number if asked. Give it a tool that calls a currency API and tell it never to state a rate it did not get from that tool.

Should the model do the conversion arithmetic?

Let the tool do it. Return the rate, the converted amount and the rate timestamp from your function, so the model only has to report numbers rather than multiply them.

Does the same tool schema work with OpenAI or Gemini?

Yes. The input is plain JSON Schema. Anthropic puts it under input_schema, OpenAI under parameters inside a function definition, and Gemini in a function declaration. Your Python implementation does not change.

Do I need to write code to give an agent exchange rates?

Not if your client supports MCP. Add the hosted server at https://allratestoday.com/api/mcp with your API key in the Authorization header, or run @allratestoday/mcp-server locally with ALLRATES_API_KEY set. The agent then gets the currency tools directly.

Which rate should an agent quote for an invoice?

An official one. A live mid-market rate is right for "what is this worth now". For an invoice, VAT or accounting question, give the agent a tool for the central bank or tax authority your rule names, and have it quote the rate_date that comes back.

Give your agent real rates

Mid-market rates for 160+ currencies refreshed every 60 seconds, plus official tables from 123 central banks and 6 tax authorities. One API key for REST and MCP.

Get a free API key

Related Articles