Guide

Exchange rates in Python with requests (and converting a price list)

A small, typed Python module that turns one reading into a converted price list.

Beginner8 min read

What this covers

Python is where most one-off financial scripts start, and most of them fail on the same three points: no timeout, no status check, and a key pasted into the file.

This guide writes the module you would keep: the key comes from the environment, the response is checked before it is trusted, and the reading is cached so a loop does not spend the balance.

Fetch with a timeout and a status check

raise_for_status() is the cheapest bug prevention in Python. Without it a 401 body parses as JSON and you spend an hour debugging a missing key.

Python · the request

import os
import requests

BASE = "https://fiatagent.base44.app"
TIMEOUT = 5

def read_rates(base: str = "USD") -> dict:
    res = requests.get(
        f"{BASE}/api/v1/rates",
        params={"base": base},
        headers={"Authorization": f"Bearer {os.environ['FIAT_AGENT_KEY']}"},
        timeout=TIMEOUT,
    )
    res.raise_for_status()
    payload = res.json()
    return {
        "rates": payload["data"]["latest"]["rates"],
        "as_of": payload["meta"]["updated_at"],
        "base": payload["meta"]["base_currency"],
        "remaining": payload["meta"]["quota"]["remaining"],
    }

Cache so a loop costs one request

A keyed call draws on your balance, so calling inside a per-row loop is a way to spend a signup bonus on a single report. Read once, keep the dict, convert many times.

functools.lru_cache gives you that in one decorator. It lives for the life of the process, which is exactly the lifetime you want for a script.

Python · one reading per process

from functools import lru_cache

@lru_cache(maxsize=1)
def cached_rates(base: str = "USD") -> dict:
    return read_rates(base)

Convert a price list

Every quote shares the base, so a cross rate is a single division. Multiply first, round last — and round to the currency's own precision rather than to two decimals for everything.

Python · pandas

import pandas as pd

rates = cached_rates("USD")["rates"]

prices = pd.DataFrame(
    {"sku": ["A-1", "B-2", "C-3"], "usd": [19.99, 45.00, 7.50]}
)

gbp_per_usd = rates["GBP"]
prices["gbp"] = (prices["usd"] * gbp_per_usd).round(2)

print(prices)

Handle the three failures that actually happen

A 400 means the request is wrong — an unpublished base code, a malformed date, or a parameter the endpoint does not accept; the body names the parameter. A 401 means the credential is missing or revoked. A 503 is transient and worth one retry after retry-after.

Retrying a 400 is the one that hurts, because it will never succeed and it spends nothing but your own time in a loop.

Python · typed failures

def safe_read(base: str = "USD") -> dict | None:
    try:
        return read_rates(base)
    except requests.HTTPError as err:
        code = err.response.status_code
        if code == 503:
            return None  # transient: let the caller retry later
        raise  # 400 / 401 are bugs, not blips
    except requests.Timeout:
        return None

Common questions

Do I need a paid account for the Python examples?

No. The keyed calls above run on the 5,000-request signup bonus that every new account gets, and nothing expires. For a quick look without any account, call /api/v1/public/rates with no headers.

Why does my conversion differ from another site by a fraction of a percent?

Different providers take their readings at different moments, and some apply spreads. Fiat Agent publishes the reading timestamp so you can compare like with like — same moment, same base, same arithmetic.

Can I request several currencies at once?

One response already contains the whole catalogue, so there is nothing to batch: read once and select the codes you need.

Run it against the real thing.

The open endpoint needs no key, so you can paste these samples into a terminal before you sign up for anything.