Currencyinfo 4.2.0: keyless by default, a rebuilt source layer, and history queries 2850× faster #78
metalisk
started this conversation in
Currencyinfo
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Currencyinfo 4.2.0 is out. It is the largest release the project has had: 45 commits, 133 files, and a rebuilt rate source layer.
This post is the technical version — what changed under the hood and why. The release notes carry the upgrade checklist.
Security audited by cryptofoundry.
Why the source layer was rebuilt
Two upstream free tiers disappeared. CryptoCompare retired its free plan on 21 May 2026 and
min-api.cryptocompare.comnow answers401without a subscription. CoinGecko's keyless public plan became unusable in practice: it throttles to 5–15 calls per minute and rate-limits unpredictably.On 4.1.x those two were the only free crypto sources. A stock configuration was therefore left with no working crypto coverage at all, and — worse — it failed on every refresh cycle rather than failing loudly once.
4.2.0 answers that with four new connectors, all keyless, and a default configuration that actually works with no credential:
Three keyless crypto sources and two keyless fiat sources means
minSources: 2is satisfiable out of the box. We verified this end to end in CI:docker-ci.ymlboots the image against the shippedconfig.default.jsoncon every pull request and asserts it returns real rates without an API key.Source independence, not just source count
Adding aggregators does not buy as much validation as it looks like. CoinGecko, CoinMarketCap, CoinPaprika and CoinLore partly share upstream data — if they agree, that is weaker evidence than four independent measurements would be.
Binance is in the default set specifically because it is an exchange, reporting its own order book rather than republishing someone else's aggregate. That is a genuinely independent signal.
It comes with a trade-off worth stating plainly: Binance quotes no direct fiat USD pairs. The connector requests
<COIN><quote_asset>markets (BTCUSDTby default) and emits pairs already named<COIN>/USD, without converting anything. During a depeg every Binance rate is off by the depeg magnitude.That is handled rather than hidden. The rates split into their own divergence group,
groupPercentagedecides whether it is reported, andstrategyresolves the pair from the healthy group. The substitution deliberately lives inside the connector and must not be expressed as a globalmappingsentry — a"USDT": "USD"mapping would apply to every source and rewrite the genuineUSDT/USDquote from the aggregators into a degenerateUSD/USD, dropping exactly the quote that makes a depeg observable.USDT/USDandUSDC/USDare in the aggregator defaults for that reason: the peg stays visible in the served data.quote_assetis restricted at the schema level to USD-pegged assets, because the connector relabels the quote instead of converting it.quote_asset: "BTC"would serve the realETHBTCprice of ~0.03 asETH/USD.Upstream APIs are hostile in interesting ways
Each new connector is shaped by a specific upstream quirk we hit while building it.
CoinPaprika has no batch-by-IDs ticker endpoint.
?ids=,?coin_ids=and?id=are all silently ignored — the request fails open and returns the full payload instead of an error — and/v1/tickers/a,banswers404. So coverage is assembled from one ranked bulk call plus one single-coin call per coin outside that rank window, withbulk_limitandmax_individual_requestsbounding the budget. Coins beyond the cap are excluded at startup with a named warning, instead of quietly burning the 20,000/month quota every cycle.CoinPaprika symbols are not unique.
ADMmatches bothadm-adamant-messenger(rank ~1460, a coin) andadm-voice-of-the-gods-by-virtuals(rank ~5688, a token). A first-match lookup would silently substitute one asset for another. Symbols are resolved among active candidates by best rank, and every returned ticker symbol is verified before its price is emitted.CoinLore's numeric IDs are reassigned across listings. An ID that pointed at one asset can later point at another. A configured ID is therefore trusted only as long as the quote it returns still carries the expected symbol; a stale ID produces a missing rate and a log line rather than a wrong price.
Binance rejects a whole batch for one bad symbol.
symbols=["BTCUSDT","ADMUSDT"]answers400— not a partial result. Without upfront validation a single misconfigured coin would take the entire source offline every cycle, so markets are resolved once at startup, and a400triggers per-candidate validation to pinpoint and drop the unlisted one.ExchangeRate-API returns HTTP 200 on failure, signalling the outcome in
{"result": "error"}. That field is treated as the only reliable success indicator.History queries: 22.8 s → 8 ms
Every
tickersindex is now date-ordered, because every/getHistoryfilter sorts by date:The bare-symbol form is an
$orover base and quote; giving both branches a date-ordered index lets the planner merge them in sorted order instead of sorting the whole matching history before the cursor can stop.Measured on a production collection of 238 million ticker documents, same host, same data, same query (
base:BTC, quote:USD, one week,limit 100), only the index differs:{base, quote, date: -1}{base, quote}(old)If you run a large history collection, build the new indexes out of band before deploying — Mongoose creates them on connect and it is real I/O. Measured on two production deployments with ~238M documents each: 17 minutes on NVMe, 50 minutes on SATA, with
indexSizeroughly doubling during the transition. The upgrade guide has the exact procedure.A correctness bug worth calling out
On 4.1.x the historical
coinpair filter was inverted.GET /getHistory?coin=ADM/USDmatchedUSD/ADMand therefore returned nothing at all, whilecoin=ADM/returned pairs with ADM as the quote.4.2.0 uses the documented
BASE/QUOTEorder everywhere. If you carried a client-side reversal workaround, remove it. Bare-symbol filters (coin=ADM) were always correct and are unchanged — they match either side.Other correctness work:
minSourcesis now measured against sources that are fresh for the requested lifetime, so a stale provider can no longer satisfy the source-count gate/getand/getHistoryschemas are.strict(): an unknown query parameter is a400rather than something silently ignored, and validation errors return400instead of500$CWIFis a real example — while typos inbase_coinsstill fail at startup/statusreportsupdatingfrom actual update state rather than inferring it from a timestamp comparison, and concurrent refresh cycles are skipped rather than overlappedSecurity
Every log line and every notification now passes through a redactor that strips API keys, bearer tokens, URI credentials and sensitive query parameters — including from Axios error URLs, which is the path that most often leaks a key in a stack trace.
This was not hypothetical. Checking a production deployment during the upgrade, 9 of 71 historical 4.1.x log files contained a live API key in plaintext, every one of them arriving through exactly that path:
Zero occurrences in the 4.2.0 logs.
Alongside that: the
logsdirectory is created0750and log files0600; log file names no longer contain colons, which made them unopenable on some filesystems; the container runs as the unprivilegednodeuser with npm, pnpm and yarn removed from the runtime image;config.jsoncis deliberately absent from the image so no layer can carry a credential; and configuration validation is strict enough to reject unknown keys, placeholder credentials and case-mismatched values before the port opens.Trivy scans every image before publication, and
multerandjs-yamlare pinned throughoverridessopnpm auditandpnpm audit --prodboth come back clean.Tested, and run in production
The suite grew from 3 spec files to 28 — 266 tests covering every source connector, the merger and its strategies, the sources manager, configuration loading and migration, the logger, the notifier, both request schemas, the exception filter, the validation pipe, the interceptor, the controller and the shared utilities.
Beyond CI, 4.2.0 has been rolled out to the two ADAMANT production instances that serve the Messenger clients — around 1,200 pairs each, over four years of history, ~238M ticker documents per node — upgraded one at a time with no simultaneous downtime and no regression in served rates.
Documentation
There is now a real documentation site: https://currencyinfo.docs.adamant.im. Quick start, installation, architecture, rate calculation, history, notifications, operations, security, troubleshooting and upgrading, plus a full REST and configuration reference and a page per rate source documenting quota, identifier form, failure mode and redistribution terms.
That last part matters and is easy to miss:
Currencyinfo is GPL-3.0 and self-hosted: your own rates service, your own infrastructure, no third party learning which coins your users hold. ADM is one symbol among the defaults — removing it changes nothing else.
Questions, bug reports and deployment war stories are welcome in this thread. Issues go to the tracker; security reports privately to
devs@adamant.im.All reactions