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.
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 NoneCommon 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.