Changelog
New endpoints, improvements, performance work and fixes across the Modan API, terminal and MCP server. Updated on every release.
- Fixed
The AI briefing and rate alerts follow the same rules as the rest of Modan
The terminal's AI Morning Briefing now describes only quotes checked in the last 24 hours, compares each provider only with providers of its own type, and never calls an official or parallel print the best rate. Its seven-day figures (average, high and trend) are the daily best of the leading type — IMTOs, where they quote the pair — taken from the same series as
/time-series. Until now they came from the corridor's last 50 recorded prices with every type mixed together, and the trend compared one provider's newest price with a different provider's oldest.When there is nothing current to brief on, the briefing now says why instead of writing one.
Rate alerts: a spread alert needs at least two current quotes. With one, the best-to-worst spread is 0 bps by arithmetic, not a sign that the market has tightened.
- NewChangedAPI
Filter rates by provider type; spreads now rank within a type
Rate reads in the REST API and the MCP server now take an optional
provider_type: one type, or a comma-separated list, fromimto,fintech_psp,commercial_bank,central_bank,non_bank_lp,bureau_de_change,crypto_venueandaggregator./rates?from=USD&to=NGN&provider_type=imtoreturns only the money-transfer operators on USD→NGN. Leave the parameter out and every type comes back, as before. A response to a filtered request lists the types it applied inprovider_types.Why rank the types apart: an IMTO's quote, a fintech's in-app rate, a commercial bank's board rate and a central bank's official print are different kinds of price from different kinds of firm, and they behave differently. Ranked together, the best money-transfer operator on a corridor could read as trailing a fintech, which says little about either.
- Changed: `spread_bps` now ranks within a provider type as well as a rate type. Each quote is measured against the best current quote from the same kind of provider publishing the same kind of price, so an IMTO is compared with IMTOs and a fintech with fintechs, and a corridor carries one 0.0 for each kind. Until now spread was measured within the rate type across every provider type. If you store or alert on
spread_bps, expect a one-off step in the series: the leader of each provider type now reads 0.0, and every other quote is measured from its own type's leader.avg_spread_bpson/corridorsandspread_bpson/rates/historyand/historicalfollow the same rule. Aprovider_typefilter never changes a quote's spread, because it is only ever measured within its own type. /convertentries now carryprovider_type, and the response addsbest_by_type: for each provider type present, the provider that delivers the most net of fees among its current executable quotes, or null when none qualifies (a central bank's official print, or a type whose only quotes are stale).bestis unchanged and remains the best across the types returned./corridorsaddsprovider_types, the types quoting each corridor, andbest_rate_by_type, the best current executable rate within each of them, null where none qualifies.best_ratestays and is the best across the types returned; readbest_rate_by_typefor a like-for-like comparison./rates/historyentries carryprovider_type.- The filter applies to
/rates,/convert,/corridors,/rates/history,/time-series,/historical,/changeand the/fetch-*family./rates/provider,/providers,/currenciesand/statusdo not take it. The independent mid never depends on it. - MCP:
get_rates,convert,fetch_rates,get_historyandlist_corridorstakeprovider_typeas an array, such as["imto"], andlist_corridorslists the provider types on each corridor. The server instructions and theget_ratesdescription now define spread as above. - An unknown type is refused, never ignored: REST answers
400with the ids it did not recognise ininvalid, and MCP returns a tool error naming the valid ones. Neither uses a request from your daily quota. Answering a typo with every type would be a silent substitution.
Backward compatible apart from the spread change: every new field is additive, nothing was removed or renamed, and a request without
provider_typestill returns every type. The one change in meaning isspread_bps, and theavg_spread_bpsbuilt from it. - Changed: `spread_bps` now ranks within a provider type as well as a rate type. Each quote is measured against the best current quote from the same kind of provider publishing the same kind of price, so an IMTO is compared with IMTOs and a fintech with fintechs, and a corridor carries one 0.0 for each kind. Until now spread was measured within the rate type across every provider type. If you store or alert on
- FixedAPI
Time series: best is the quote standing at each bucket's end
beston/time-seriesis now the highest current executable quote standing at the end of each bucket: the end of the day or hour, or the end of your window for the last one. It used to be the highest price recorded during the bucket, which was wrong in two ways. Our feeds record a new price only when it moves, so a provider holding the best price all day added nothing to that day, andbestwas the best of whichever providers happened to reprice. And it included every kind of price, so a central bank's official print, which nobody can deal on, could be the maximum.- Each provider's quote at a bucket's end is its latest price at or before that moment, and it counts only if it was current then, under the same rule as
stale: checked within the 24 hours before, with Saturday and Sunday (UTC) not counting for commercial and central banks. Only interbank, retail and p2p prices count. It is the rule/changeapplies at each end of its period, applied at every bucket end. samplesis now the number of quotesbestwas taken from, and is 0 exactly whenbestis null. It used to count the rows recorded during the bucket, which, since a row is recorded only when a price moves, measured how often prices changed rather than how many providers were quoting.- What moves: hourly series lose their holes. Over the last week of GBP/NGN, 55 of 168 hours had no
bestbecause no provider repriced in them, and on 82 morebestmissed a better price that was standing but had not moved; every hour now has one. Daily values move too, and mostly down, because the old figure was the day's highest price even when it had been replaced before the day ended: 159 of GBP/NGN's last 321 daily values change, by a median of 28.7 basis points. On 14 and 17 September GBP/KES's dailybestwas the Central Bank of Kenya's official print. bestis null, andsamples0, when nothing current and executable stood at a bucket's end: a price nobody has confirmed for 24 hours is never carried forward. On GBP/NGN that is 43 of the last 366 days, nearly all of them Sundays before mid-August, when no GBP/NGN quote had been confirmed in the 24 hours before the day ended; those days had nobestbefore either. A bucket is listed while the corridor has any quote checked within 7 days of its end, and a pair with none in the window returns a mid-only series, as before.
- Each provider's quote at a bucket's end is its latest price at or before that moment, and it counts only if it was current then, under the same rule as
- FixedAPIBreaking
Stale quotes are never “best”
A quote nobody has confirmed for 24 hours is now marked stale wherever Modan shows current rates — the REST API, the MCP tools, rate alerts and the site — and it can no longer be named the best rate. Until now nothing asked how old a quote was: on 28 Sep a USDC→GHS quote last checked on 10 Jul was that corridor's best on the site, in
/rates,/convertand/corridors, and in the MCP tools, and rate alerts could have named it. Staleness is judged onlast_checked, when we last confirmed the provider was still offering the price, never on when the price last moved, so a corridor holding a steady rate stays current. Commercial-bank and central-bank boards publish on weekdays only, so their quotes age on a weekday clock: Saturday and Sunday (UTC) do not count, and Friday's close stays current through the weekend. A quote not checked for 7 days is no longer returned by any current read; its history stays in/rates/history./rates,/convert,/rates/providerand/historicalreturnstaleon every quote and astale_count. A stale quote is still listed, withspread_bps: null, and every other quote'sspread_bpsis now measured against the best current quote of its rate type./historicaljudges staleness at the date you ask for./convertnever picks a stale quote asbest, and its entries now carrylast_checkedandlast_changed.- Type change on `/corridors`:
best_rateandavg_spread_bpsare nownull, where they used to be0, when a corridor has no current executable quote. If your code treats them as numbers, handle null.provider_countnow counts current quotes only, a newstale_countcounts the rest, and a corridor with nothing checked in 7 days is left out.spread_bpson/ratesand/convertcan now be null too, for stale quotes. provider_beston/fetch-one,/fetch-multi,/fetch-matrix,/fetch-many-to-oneand MCPfetch_ratesis the best current executable quote, and null when none qualifies. It was a plain maximum, which could name a central bank's official print or a quote unchecked for 80 days./changenow compares the best executable quote that was current at each end of the period.- Fixed for Free and Individual keys: a quote whose price had not moved for 7 days, while still being confirmed, was missing from
/rates,/convert,/rates/providerand the MCPget_ratesandconverttools. On 28 Sep that hid Pay Angel's USD→NGN quote and three GBP→XOF quotes from every hourly key. They are back, and/historicaland/changeno longer lose steady prices either. A quote confirmed after theas_ofhour now reportslast_checkedasas_ofrather than the time its price was set, which had made steady prices look days old. - MCP:
get_ratesis now titled "Get corridor rates", because hourly keys get hourly data. The tool descriptions and server instructions explain stale, andlist_corridorscounts only current quotes inproviders, reporting the rest asstale_count, as/corridorsdoes./statuscounts corridors with a quote checked in the last 7 days, and rate alerts name a best rate only from executable quotes checked in the last 24 hours.
- Improved
Decimal slips are corrected in one pass
A small number of archived observations were recorded a clean factor of ten from what every other provider quoted the same day — 1,371,027 where the market was near 1,364, which is the same digits with a comma read as a thousands separator rather than a decimal point. Admin can now restore the point across all of them in one action. It is deliberately narrow: only a clean power of ten qualifies, the digit sequence is never altered, an observation whose corrected value would need more precision than we store is left for a person, and each correction is still checked against what other providers quoted at that moment — anything that does not fit is skipped and reported rather than written. Corrections are appended at the original timestamp as before, and any of them can be reversed in one click, putting the original observation back.
- Improved
Corrections are appended, never written over
When a recorded observation turns out to be wrong and we can establish what the provider actually published, the correction is now appended at the same timestamp as the observation it replaces. The original moves to the same withdrawal record used elsewhere — keeping the value it held, who corrected it and why, plus a pointer to what stands in its place — so the series reads correctly from then on while the mistake stays on the record. The admin rate table's edit form, which used to overwrite the row, is gone: there is no longer any path that changes a published number in place. A correction that is itself implausible against what other providers quoted at that moment is refused outright, and the original observation stays exactly where it was.
- Improved
Suspected bad rates can be withdrawn, and fewer get in
A handful of observations in the archive were recorded at ten, a hundred or a thousand times their true value — parsing slips rather than prices, and they distorted any chart or time series that included them. Admin now has a review screen that ranks every observation against what other providers quoted on the same corridor that day, says why each one looks wrong, and lets the team withdraw the confirmed errors. Withdrawn observations are not deleted: the original value, its timestamp, who withdrew it and why are all kept, and it can be put back. Nothing is ever edited in place. Separately, the 30% plausibility check that already guarded the automated feed now runs in the database, so it covers the manual rate form and the bulk upload too; it stands aside when a corridor has too few quotes to judge against, so a genuine market-wide move is never blocked.
- New
Eight guides on African FX pricing, with the data as the worked example
modan.io/guides is a new section of plain-language explainers, each answering one question people actually ask: what the official, interbank, retail and parallel naira rates each mean and which you can transact at; how to read spread_bps and vs_mid_bps in basis points; how remittance providers set their rates; how a treasurer can benchmark a bank's quote against the provider book and the independent mid; getting NGN rates by API in Python and JavaScript; giving an AI agent live rates through MCP; how USDT→NGN stablecoin corridors are priced and why they carry no mid; and how Modan collects and validates its data. Every guide links to the live corridor, currency and provider pages it discusses, carries a FAQ, and is real HTML.
- NewImproved
An MCP setup page, a registry manifest, and live facts in llms.txt
modan.io/docs/mcp is a new page with one job: connect Modan to Claude Code, Cursor, VS Code or any other MCP client in a minute. It lists the seven tools straight from the server's own definitions (so it can never describe a tool that does not exist), shows the exact config for each client, gives example prompts, explains how to read spread_bps, vs_mid_bps, rate_type and data_freshness in the answers, and says plainly which clients cannot send an API-key header today (ChatGPT connectors — use a Custom GPT Action with openapi.json instead). A server.json manifest for the official MCP Registry ships in the repository. llms.txt now opens with a facts block (providers, corridors, currencies, latest observation) that is regenerated on every deploy, plus an index of every kind of public page, and both llms files name the plans as Free / Individual / Team.
- New
Corridor, currency and provider hub pages
Three new kinds of public page, all prerendered and all drawn from the same live rate book. /corridors lists every currency pair Modan tracks, grouped by the currency the money lands in, with the best executable quote, who quotes it, how many providers do, and when it was observed. /currency/ngn (and one page per currency, 19 in all) shows every corridor into and out of that currency plus the providers quoting it. /providers lists every provider with its institution type, the kind of price it publishes, how many corridors it quotes and on how many it has the best executable rate; /providers/lemfi (one per provider, 21 in all) ranks that provider on every corridor it quotes, with its distance from the best in basis points. Each carries a dated summary, a FAQ and an API snippet, and the header now has a Corridors link. The sitemap grew from 360 to 402 pages.
- NewImproved
Every public page is now real HTML, and every corridor has a page worth reading
Until now the site was a JavaScript application that handed crawlers and AI assistants an empty page. The build now renders every public page to static HTML — the home page, docs, pricing, help, changelog, all 80 corridor pages and every provider-on-a-corridor page (359 pages) — with the live rate book baked in and clearly dated, so search engines, ChatGPT, Claude and anyone fetching a URL see the same table you do. Each corridor page now reads as a report: a plain-language summary of the best executable quote, the spread across providers and the independent mid, a short FAQ, an API snippet, and links to related corridors; provider pages say where that provider ranks and link to its other corridors. Structured data (Organization, Dataset, FAQ, breadcrumbs, product offers, WebAPI) and a sitemap of every page ship with it, and corridor URLs are now lowercase (old uppercase links still work). Nothing changes in the browser except that pages paint before the JavaScript arrives.
- Fixed
Home page coverage, live rates and counts no longer vanish
For anonymous visitors the home page could load without its corridor coverage globe, live-rates strip and provider/corridor counts. All three came from one read of the whole current rate book, which is a scan of the entire append-only rates table and exceeded the database's 3-second limit whenever it ran cold — the page then quietly rendered as if there were no data. The page now reads the same lightweight snapshot the API serves (about a hundredth of the work, well under a second), and if that read ever fails it says so in place of the coverage section instead of hiding it.
- Improved
A cleaner landing page and one navigation everywhere
The landing page now follows one rhythm — a single column width, one section padding scale, one heading style — so the coverage globe, the pricing preview and the footer line up with everything else instead of each choosing their own spacing. Every public page (home, pricing, API docs, changelog, help, corridor pages) shares the same header: API Docs · Pricing · Help · Changelog, with the current page marked and a proper menu on phones, where the links used to disappear entirely. The footer is aligned to the page column and now links the API status endpoint, the MCP docs and the most-used corridor pages.
- ImprovedAPI
New daily API quotas: Free 50, Individual 250, Team 1,000
Daily request allowances are now Free 50 / Individual 250 / Team 1,000 per UTC day (previously 25 / 500 / 5,000). The free tier doubles so an evaluation can run a real corridor sweep; Individual and Team are sized to the request patterns we actually see on those plans. Everything else is unchanged: the allowance is shared across all of an account's keys, MCP tool calls count against it, every response carries
X-RateLimit-Limit/X-RateLimit-Remaining/X-RateLimit-Reset, requests beyond the limit are refused rather than billed, and data freshness stays hourly on Free/Individual and real-time on Team. The new numbers apply to existing keys immediately — no new key needed. - Fixed
Spread and “Best” now respect rate type
The terminal grid and the public corridor pages measured spread against the highest number on the board, so an official or parallel print could sit at 0.0 as the "best" rate — a price nobody can actually get. Spread is now measured within a rate type, exactly as the API's
spread_bpsis, and Best only ever names the best executable quote. Official and parallel prints are labelled· refand are never ranked; a provider page for one says so instead of showing a rank. - NewAPIBreaking
Provider taxonomy + rate_type: spread now compares like with like
Every provider now carries two independent labels. provider_type says what kind of institution it is — central_bank, commercial_bank, non_bank_lp, imto, fintech_psp, crypto_venue, bureau_de_change or aggregator. rate_type says what kind of PRICE it publishes — official, interbank, retail, p2p or parallel. A commercial bank may post a retail board rate or an interbank one, so the two are orthogonal.
spread_bps is now measured WITHIN a rate_type. A corridor carrying several kinds of price therefore carries several 0.0 spreads, one per kind. Previously a central bank's official reference was ranked against executable retail quotes, which reported dispersion nobody could ever have traded.
official and parallel prices are not executable. They are still returned and labelled, but excluded from best on /convert and from best_rate and avg_spread_bps on /corridors — naming a published reference as the best available rate would be recommending a price nobody can get.
- BREAKING: provider_type values changed. 'mto' is now 'imto' (the licence the CBN issues) and 'fintech' is now 'fintech_psp'. If you switch on those strings, update them.
- /rates, /convert and /rates/history now return rate_type and provider_type on each entry; /providers returns rate_type alongside provider_type.
- Applies to the REST API, the MCP tools and the terminal alike.
- NewAPI
Tiered data freshness + new API quotas
Data freshness is now part of the plan ladder: Free and Individual API keys serve rates as of the top of the current UTC hour, while Team keys serve every observation in real time. Responses always state which you got via data_freshness (plus as_of when hourly) and an X-Data-Freshness header.
- Daily API quotas are now Free 25 / Individual 500 / Team 5,000 requests.
- Applies to the REST API and MCP tools; the terminal keeps live ticks on every plan.
- ImprovedAPI
Free tier raised to 25 requests/day
The free daily quota is now 25 requests/day, up from 10. Pro (10,000/day) and Enterprise (100,000/day) are unchanged.
The quota is shared across all of an account's keys and resets at 00:00 UTC. Every response still carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset. - New
Email alert when you hit your daily API limit
When an account crosses its daily quota, the owner now gets a one-per-day email with a direct upgrade link — so a
429never comes as a surprise.Track consumption any time from Developers → My Keys or via
GET /api/v1/admin/usage, which reports used / remaining / reset plus a per-key breakdown and 7-day history and never counts against your quota. - NewAPI
Developer FX endpoint suite: fetch-*, time-series, historical, change
A fastforex-style convenience layer for building fintech products on Modan:
GET /fetch-one,/fetch-multi,/fetch-matrix,/fetch-many-to-one— mid-market rates for one or many pairs in a single call (one quota unit regardless of pair count), with the best tracked provider attached where the pair is a covered corridor.GET /time-series— daily (P1D) or hourly (PT1H) buckets of mid + best provider.GET /historical— a corridor snapshot as of a past date, andGET /change— absolute and % move of mid and best over a period.GET /rates/provider— every corridor and current rate a single provider quotes.GET /admin/usageandGET /status(no key required) for metering and platform health.
- Performance
Sub-second API responses
Reworked the request path — single round-trip authentication, reads issued in parallel with auth, background usage logging and a loose-index-scan snapshot — bringing every public endpoint under 1 second warm (from ~2.4s).
No changes required on your side.
- New
MCP server — call Modan from AI agents
Modan now runs a native Model Context Protocol server at
https://modan.io/api/mcp. Claude, Cursor, Codex and any MCP client get live African FX as tools —get_rates,convert,fetch_rates,get_history,list_corridors,list_providers,list_currencies— authenticated with the same API key and daily quota.Errors come back as readable messages an agent can act on.
- New
Independent mid-market reference
Rate responses now include an independent mid-market benchmark when a fresh reference exists: top-level
mid_rate/mid_source/mid_fetched_at, and per-providervs_mid_bps(basis points versus that mid; negative means the provider pays out below mid).These fields are omitted when no recent reference is available — absence is explicit, never fabricated.
- NewImproved
New endpoints: /convert and /currencies, paginated history, JSON errors
GET /convertreturns per-provider gross and net-of-fee delivered amounts and flags the provider with the best value for the recipient.GET /currencieslists active currencies and the corridors currently served.GET /rates/historyis now paginated (limit,offset,order,has_more) withperiod= 1d / 7d / 30d / 90d.- Unknown paths now return a JSON
404with an actionable message instead of HTML.
- New
Rate ingestion API + CSV bulk upload
Data-team accounts can push observations via
POST /api/v1/rates(one object or an array of up to 100, atomic per batch) or the admin console's CSV/JSON bulk uploader. Ingestion does not consume the read quota, and every write is append-only and audited. - New
Rate and spread alerts
Set rate-above / rate-below and spread-above / spread-below alerts per corridor in the terminal. The evaluator runs every 15 minutes and can notify you in-app, by email, or both.
- NewAPI
Public REST API launch
Modan is now a developer platform: sign up, grab an API key, and pull live African FX in minutes. Launch endpoints include
GET /rates,/corridorsand/providers, authenticated with anX-API-Keyheader and metered by daily quota.Machine-readable references ship alongside: openapi.json (OpenAPI 3.1), llms.txt and llms-full.txt.