The XML rates feed publishes Kotova X rates in the XML format that rate monitors such as BestChange read. It is public and needs no key. Its rates are indicative: the rate your user gets is the one in a quote.
Fetching the feed
Section titled “Fetching the feed”There are two documents, one per rate type:
| URL | Rates |
|---|---|
https://api.kotova.io/x/v1/rates/variable.xml |
Variable rates (variable): the final amount is set when the deposit arrives. |
https://api.kotova.io/x/v1/rates/fixed.xml |
Fixed rates (fixed). |
Both answer GET and HEAD, need no key and carry no weight, and send
Access-Control-Allow-Origin: *. Send no Authorization header and no query string:
any query string at all, including an empty one such as ?&, is a 400
invalid_request, so the URLs cannot be cache-busted.
curl -si https://api.kotova.io/x/v1/rates/variable.xmlconst res = await fetch('https://api.kotova.io/x/v1/rates/variable.xml');const status = res.headers.get('X-Kotova-Feed-Status'); // "ok" or "degraded"const xml = await res.text();import requests
res = requests.get("https://api.kotova.io/x/v1/rates/variable.xml", timeout=10)res.raise_for_status()status = res.headers["X-Kotova-Feed-Status"] # "ok" or "degraded"xml = res.textA successful answer has these headers (values from an example):
HTTP/2 200content-type: application/xml; charset=utf-8cache-control: public, s-maxage=10, stale-while-revalidate=20etag: "9c1f0e4b7a2d6385"last-modified: Tue, 10 Nov 2026 10:15:04 GMTx-kotova-feed-status: okx-kotova-feed-sources: ccecash,ffiox-kotova-feed-generated-at: 2026-11-10T10:15:04.512Zx-request-id: req_8fK2mQ9xLp3vTz7nWc1bYd4h| Header | Meaning |
|---|---|
Cache-Control |
The CDN keeps a document for 10 seconds, and may serve it for 20 more while it fetches a new one. |
ETag |
A strong validator, " + 16 hexadecimal characters + ". Send it back in If-None-Match to get a 304 Not Modified with no body when nothing has changed. |
Last-Modified |
When the document was built. If-Modified-Since also gives a 304, when you send no If-None-Match. |
X-Kotova-Feed-Status |
ok when the rates of every source the feed is built from are fresh and the document has at least one line; otherwise degraded: some sources’ rates are missing, or the document is empty. |
X-Kotova-Feed-Sources |
The slugs of the sources whose rates are in the document, separated by commas, or none. |
X-Kotova-Feed-Generated-At |
When the document was built, in ISO 8601 with milliseconds. |
The other answers:
| Status | Code | When |
|---|---|---|
| 304 | If-None-Match or If-Modified-Since matches the current document. |
|
| 400 | invalid_request |
A query string was sent. details.reason is unknown_parameter. |
| 404 | not_found |
A file name other than variable.xml or fixed.xml, or the feed is switched off. |
| 405 | method_not_allowed |
A method other than GET, HEAD or OPTIONS. |
| 429 | rate_limited |
More than 120 requests a minute from one IP address reached the server. Answers from the CDN do not count, and BestChange’s monitoring robots are exempt. |
| 503 | service_unavailable |
The API is not available. |
Try itGET/rates/{file}No key
Format
Section titled “Format”The feed uses BestChange’s XML rates format: an XML declaration, a rates element, and
one item element per line for each direction, sorted by from and then by to.
<?xml version="1.0" encoding="UTF-8"?><rates><item><from>BTC</from><to>USDTTRC20</to><in>1</in><out>68021.33412</out><amount>412000.5</amount><minamount>0.00092</minamount><maxamount>1.5</maxamount><tofee>0.1</tofee></item><item><from>ETH</from><to>BTC</to><in>1</in><out>0.0361</out><amount>12.5</amount><minamount>0.01</minamount><maxamount>40</maxamount></item><item><from>XMR</from><to>BTC</to><in>1</in><out>0.002511873</out><amount>18.5</amount><minamount>0.25</minamount><maxamount>310</maxamount><tofee>0.00012</tofee><param>manual</param></item></rates>The elements of an item always come in this order:
| Element | Meaning |
|---|---|
from |
The currency sent, as a BestChange code. |
to |
The currency received, as a BestChange code. |
in |
Always 1. |
out |
The amount of to paid out per 1 unit of from, before tofee. |
amount |
The reserve: how much of to the source reports it can pay out, in units of to. |
minamount |
The smallest amount of from the source accepts. |
maxamount |
The largest amount of from the source accepts. |
tofee |
Optional. A fee taken from the payout: an amount in units of to, or a percentage written as N%. |
param |
Optional. BestChange flags passed on from the source, separated by commas: manual, verifying, reg, otherin or otherout. |
Numbers are plain decimals: no exponent, no sign, no unit, no spaces and no thousands
separators. The document has no comments, no other elements and no per-line attribution;
which sources contributed is in the X-Kotova-Feed-Sources header. When there is nothing
to publish, the feed is the valid empty document:
<?xml version="1.0" encoding="UTF-8"?><rates></rates>What a line means
Section titled “What a line means”A line says that, when the document was built, a source the API routes to was offering
this direction: it would take between minamount and maxamount of from, and pay out
about out units of to for each unit sent, minus tofee.
A line is indicative, not an offer. The feed is built from rates that sources publish themselves, and only some sources publish them. A quote asks every source that serves the pair, at the moment your user asks, so the rate in a quote can be higher or lower than the line, and with a variable rate the final amount is set when the deposit arrives.
How a line is chosen
Section titled “How a line is chosen”A direction is published only when a quote could be routed to the source right now: both
currencies are in the code table and active in the catalogue, the
source is active for the API and not paused, and it serves the direction (it is in the
from leg’s send_sources and the to leg’s receive_sources).
Each line is then checked. It is left out unless out is positive, minamount is
positive and below maxamount, the reserve is positive, and the fee at the minimum
amount is at most 10% of the payout there.
When more than one source offers the same direction, one of them is published, with all of its values: the rate, the limits, the reserve and the fee always come from the same source and are never mixed or averaged.
- Each candidate’s payout is
x × out − tofee(orx × outminus the percentage) for an amountx. - When their ranges overlap, both are compared at the smallest amount both accept, the higher of the two minimums. When they do not, each is compared at its own minimum, per unit sent.
- The higher payout wins. A tie goes to the wider range, then to the lower minimum.
- A winner whose rate is more than 5% above the other candidate’s is treated as suspect, and the other candidate is published instead.
Rates are only compared with each other, never with a market price.
Rounding
Section titled “Rounding”Every rounding goes against your user, so a line never promises more than its source offered:
| Value | Rounding |
|---|---|
out |
Cut off (not rounded) to 10 significant digits, with at most 18 decimal places. |
minamount |
Rounded up to min(decimals, 8) decimal places, where decimals are those of the from leg. |
maxamount |
Rounded down to the same number of decimal places. |
amount |
Rounded down to min(decimals, 8) decimal places of the to leg. |
tofee |
Rounded up to min(decimals, 8) decimal places of the to leg. A percentage is published as the source gave it. |
Freshness
Section titled “Freshness”The feed never publishes stale rates:
- A document is rebuilt from the sources’ latest rates when it is more than 10 seconds old and someone asks for it. Up to 60 seconds old, it is still served while the new one is built; older than that, the request waits for the rebuild. A document more than two minutes old is never served.
- A source’s rates are left out once they are more than two minutes old. When no
source’s rates are fresh, the feed is the empty document, with
X-Kotova-Feed-Status: degraded. - On top of that, the CDN caches each document for 10 seconds (
s-maxage=10).
X-Kotova-Feed-Generated-At tells you exactly how old the document you received is.
Polling more often than every 10 seconds returns the same document; use If-None-Match
so that an unchanged one costs a 304.
Currency codes
Section titled “Currency codes”The feed uses BestChange currency codes, not the API’s identifiers. It publishes only
the codes in this table; a direction whose currency has no code here is left out. Each
code stands for one leg, with currency and network as in
Currencies, networks and icons.
| BestChange code | currency |
network |
|---|---|---|
BTC |
BTC |
btc |
ETH |
ETH |
eth |
ETHBEP20 |
ETH |
bsc |
ETHARBTM |
ETH |
arbitrum |
ETHOPTM |
ETH |
op |
ETHBASE |
ETH |
base |
BNBBEP20 |
BNB |
bsc |
LTC |
LTC |
ltc |
BCH |
BCH |
bch |
XMR |
XMR |
xmr |
SOL |
SOL |
sol |
TRX |
TRX |
trx |
XRP |
XRP |
xrp |
DOGE |
DOGE |
doge |
DASH |
DASH |
dash |
ZEC |
ZEC |
zec |
GRAM |
GRAM |
ton |
USDTERC20 |
USDT |
eth |
USDTTRC20 |
USDT |
trx |
USDTBEP20 |
USDT |
bsc |
USDTSOL |
USDT |
sol |
USDTPOLYGON |
USDT |
matic |
USDTTON |
USDT |
ton |
USDTARBTM |
USDT |
arbitrum |
USDCERC20 |
USDC |
eth |
USDCSOL |
USDC |
sol |
USDCBEP20 |
USDC |
bsc |
USDCBASE |
USDC |
base |
USDCARBTM |
USDC |
arbitrum |
USDCPOLYGON |
USDC |
matic |
POL |
POL |
matic |
ADA |
ADA |
ada |
XLM |
XLM |
xlm |
Bitcoin on Lightning (BTCLN) is not in the feed in v1. Codes may be added to this
table over time.