Changelog

    New endpoints, improvements, performance work and fixes across the Modan API, terminal and MCP server. Updated on every release.

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

    2. 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, from imto, fintech_psp, commercial_bank, central_bank, non_bank_lp, bureau_de_change, crypto_venue and aggregator. /rates?from=USD&to=NGN&provider_type=imto returns 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 in provider_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_bps on /corridors and spread_bps on /rates/history and /historical follow the same rule. A provider_type filter never changes a quote's spread, because it is only ever measured within its own type.
      • /convert entries now carry provider_type, and the response adds best_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). best is unchanged and remains the best across the types returned.
      • /corridors adds provider_types, the types quoting each corridor, and best_rate_by_type, the best current executable rate within each of them, null where none qualifies. best_rate stays and is the best across the types returned; read best_rate_by_type for a like-for-like comparison. /rates/history entries carry provider_type.
      • The filter applies to /rates, /convert, /corridors, /rates/history, /time-series, /historical, /change and the /fetch-* family. /rates/provider, /providers, /currencies and /status do not take it. The independent mid never depends on it.
      • MCP: get_rates, convert, fetch_rates, get_history and list_corridors take provider_type as an array, such as ["imto"], and list_corridors lists the provider types on each corridor. The server instructions and the get_rates description now define spread as above.
      • An unknown type is refused, never ignored: REST answers 400 with the ids it did not recognise in invalid, 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_type still returns every type. The one change in meaning is spread_bps, and the avg_spread_bps built from it.

    3. FixedAPI

      Time series: best is the quote standing at each bucket's end

      best on /time-series is 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, and best was 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 /change applies at each end of its period, applied at every bucket end.
      • samples is now the number of quotes best was taken from, and is 0 exactly when best is 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 best because no provider repriced in them, and on 82 more best missed 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 daily best was the Central Bank of Kenya's official print.
      • best is null, and samples 0, 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 no best before 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.
    4. 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, /convert and /corridors, and in the MCP tools, and rate alerts could have named it. Staleness is judged on last_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/provider and /historical return stale on every quote and a stale_count. A stale quote is still listed, with spread_bps: null, and every other quote's spread_bps is now measured against the best current quote of its rate type. /historical judges staleness at the date you ask for. /convert never picks a stale quote as best, and its entries now carry last_checked and last_changed.
      • Type change on `/corridors`: best_rate and avg_spread_bps are now null, where they used to be 0, when a corridor has no current executable quote. If your code treats them as numbers, handle null. provider_count now counts current quotes only, a new stale_count counts the rest, and a corridor with nothing checked in 7 days is left out. spread_bps on /rates and /convert can now be null too, for stale quotes.
      • provider_best on /fetch-one, /fetch-multi, /fetch-matrix, /fetch-many-to-one and MCP fetch_rates is 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. /change now 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/provider and the MCP get_rates and convert tools. On 28 Sep that hid Pay Angel's USD→NGN quote and three GBP→XOF quotes from every hourly key. They are back, and /historical and /change no longer lose steady prices either. A quote confirmed after the as_of hour now reports last_checked as as_of rather than the time its price was set, which had made steady prices look days old.
      • MCP: get_rates is now titled "Get corridor rates", because hourly keys get hourly data. The tool descriptions and server instructions explain stale, and list_corridors counts only current quotes in providers, reporting the rest as stale_count, as /corridors does. /status counts 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.
    5. 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.

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

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

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

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

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

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

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

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

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

    15. 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_bps is, and Best only ever names the best executable quote. Official and parallel prints are labelled · ref and are never ranked; a provider page for one says so instead of showing a rank.

    16. 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.
    17. 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.
    18. 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-Remaining and X-RateLimit-Reset.

    19. 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 429 never 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.

    20. 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, and GET /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/usage and GET /status (no key required) for metering and platform health.
    21. 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.

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

    23. 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-provider vs_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.

    24. NewImproved

      New endpoints: /convert and /currencies, paginated history, JSON errors

      • GET /convert returns per-provider gross and net-of-fee delivered amounts and flags the provider with the best value for the recipient.
      • GET /currencies lists active currencies and the corridors currently served.
      • GET /rates/history is now paginated (limit, offset, order, has_more) with period = 1d / 7d / 30d / 90d.
      • Unknown paths now return a JSON 404 with an actionable message instead of HTML.
    25. 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.

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

    27. 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, /corridors and /providers, authenticated with an X-API-Key header and metered by daily quota.

      Machine-readable references ship alongside: openapi.json (OpenAPI 3.1), llms.txt and llms-full.txt.