> For the complete documentation index, see [llms.txt](https://docs.swapkit.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.swapkit.dev/guides/catalogue-sync-tokens-networks-and-paths.md).

# Catalogue sync - tokens.json, networks.json and paths

Sync SwapKit's whole token and route catalogue in a few dozen static-file requests — which tokens exist, which chains, and what each token can be swapped into — instead of calling /swapTo once per tok

If you mirror SwapKit's catalogue into your own backend — to show users only the pairs SwapKit can quote, or to route requests between aggregators — sync these three static files instead of calling [`/swapTo`](/api-reference/swapto-request-buy-swap-options.md) once per token.

| File                      | What it holds                                                                           |
| ------------------------- | --------------------------------------------------------------------------------------- |
| `tokens.json`             | Every token SwapKit supports, with its identifier, chain, contract address and decimals |
| `networks.json`           | Every chain in `tokens.json`, its native token and how many tokens it has               |
| `paths/{srcChainId}.json` | For every token on one source chain, the tokens it can be swapped into                  |

All three are static JSON files served from SwapKit's CDN at `https://tokens.swapkit.dev`. They need no API key, they're compressed automatically, and they support conditional requests. A full sync is one `tokens.json`, one `networks.json`, and one paths file per chain — 37 requests today — and a sync where nothing changed is the same requests answered with `304 Not Modified`.

{% hint style="info" %}
Routes are listed at the **asset** level: `ETH.LINK` can reach almost every Ethereum token, while `BTC.BTC` reaches only the Ethereum tokens a cross-chain route delivers. A route in these files is one SwapKit can quote; whether a particular request succeeds still depends on its amount — see [What the files don't tell you](#what-the-files-dont-tell-you).
{% endhint %}

***

## Endpoints

| Method | URL                                                  | Returns                           |
| ------ | ---------------------------------------------------- | --------------------------------- |
| `GET`  | `https://tokens.swapkit.dev/tokens.json`             | Every token                       |
| `GET`  | `https://tokens.swapkit.dev/networks.json`           | Every chain                       |
| `GET`  | `https://tokens.swapkit.dev/paths/{srcChainId}.json` | Every route from one source chain |

* **No authentication.** Don't send an `x-api-key`; these requests don't count against your API key's rate limits.
* **`HEAD` works too**, if you only want the current `ETag` and `Last-Modified`.
* `{srcChainId}` is a `chainId` from `networks.json`, used as-is: `paths/1.json`, `paths/42161.json`, `paths/bitcoin.json`, `paths/solana.json`.
* **A `404` on a paths file means "no routes yet".** A chain newly added to `networks.json` can be listed a few hours before its paths file is first written; treat it as having no routes and try again on the next sync.
* **Fetch only the chains `networks.json` lists.** A paths file for a chain that has since left the catalogue may still be served; ignore it.

## Sync flow

1. `GET /tokens.json` — load every token, keyed by `identifier`.
2. `GET /networks.json` — the chains, and so the list of paths files to fetch.
3. For each `chainId` in `networks.json`: `GET /paths/{chainId}.json`.

Make every request conditional (next section). Read `tokens.json` before the paths files, so every identifier a paths file names can be resolved.

***

## Conditional requests and `ETag`

Every response carries an `ETag` and a `Last-Modified` header. A file is rewritten only when its content changes, so both stay the same between changes, and a request that presents them gets `304 Not Modified` with no body.

```http
GET /paths/1.json HTTP/1.1
Host: tokens.swapkit.dev
Accept-Encoding: gzip, br
If-None-Match: W/"5f1c0e7b9a2d4c8e6b3a1f0d9e8c7b6a"
```

```http
HTTP/2 304
etag: W/"5f1c0e7b9a2d4c8e6b3a1f0d9e8c7b6a"
cache-control: public, max-age=300
```

* **Store the `ETag` per file, exactly as received, and send it back in `If-None-Match`.** Each file — every paths file included — has its own `ETag`; one file changing says nothing about the others.
* **Weak and strong forms both match.** A compressed response carries a weak tag (`W/"…"`), an uncompressed one the same value without the `W/` prefix. Either form gets a `304` whichever encoding you ask for, so you don't need to normalise it.
* **`If-Modified-Since` works as well**, with the `Last-Modified` value from the previous response. Use one or the other; `ETag` is the more precise.
* **Treat the `ETag` as an opaque string.** Don't parse it or compare it to anything but a previous `ETag` for the same URL.
* **On `304`, keep what you have.** There is no body; reuse your stored copy of that file.
* **`version` is for your logic, the `ETag` is for transport.** Use the `ETag` to decide whether to download a file, and `version` (see [Freshness and versioning](#freshness-and-versioning)) to decide what a change means for the data you've stored.

## Compression

Every file is served compressed — **brotli** (`br`) or **gzip**, whichever your client lists in `Accept-Encoding` — and nothing on SwapKit's side needs configuring. What you need to check is your HTTP client: some decompress for you, some don't.

**Prefer brotli where your client supports it.** The paths files repeat the same identifiers across their destination lists, and brotli exploits that far better than gzip: `paths/1.json` is 773 KB uncompressed, 400 KB with gzip and **44 KB with brotli**. A full sync is about 490 KB with brotli and about 1 MB with gzip (see [File sizes](#file-sizes)).

| Client                                     | Asks for compression                                             | Decompresses for you                                                                   |
| ------------------------------------------ | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `fetch` — browsers, Node.js 18+, Bun, Deno | Yes (`br`, `gzip`)                                               | Yes                                                                                    |
| Node.js `https` / `http` modules           | No — set the header                                              | No — use `node:zlib`                                                                   |
| Python `requests`                          | Yes (`gzip`; `br` once the `brotli` package is installed)        | Yes                                                                                    |
| Python `urllib.request`                    | No — set the header                                              | No — use `gzip` (the standard library has no brotli decoder)                           |
| Go `net/http`                              | Yes (`gzip` only), **unless you set `Accept-Encoding` yourself** | Yes, on the same condition — for `br`, set the header and decode with a brotli package |
| Java `java.net.http.HttpClient`            | No — set the header                                              | No — use `GZIPInputStream` (or a brotli library such as Brotli4j)                      |
| OkHttp (Kotlin / Android)                  | Yes (`gzip`)                                                     | Yes                                                                                    |
| `curl`                                     | Only with `--compressed`                                         | Only with `--compressed`                                                               |

Each example fetches a file compressed, then re-requests it with `If-None-Match` and gets a `304`, except the Go (brotli) tab, which shows only the decode.

{% tabs %}
{% tab title="JavaScript (fetch)" %}

```typescript
// fetch sends `Accept-Encoding: br, gzip` and decompresses the body itself:
// nothing to configure, and res.json() gets plain JSON.
const url = "https://tokens.swapkit.dev/tokens.json";

const res = await fetch(url);
const { tokens } = await res.json();
const etag = res.headers.get("ETag"); // W/"…" — the response was compressed

const again = await fetch(url, { headers: { "If-None-Match": etag! } });
console.log(again.status); // 304 while the file is unchanged
```

{% endtab %}

{% tab title="Node.js (https)" %}

```javascript
import https from "node:https";
import zlib from "node:zlib";

// node:https does not decompress: ask for br/gzip and pipe through the matching decoder.
const getJson = (url, headers = {}) =>
  new Promise((resolve, reject) => {
    https
      .get(url, { headers: { "Accept-Encoding": "br, gzip", ...headers } }, (res) => {
        if (res.statusCode === 304) {
          res.resume();
          return resolve({ status: 304, etag: res.headers.etag });
        }
        const encoding = res.headers["content-encoding"];
        const decoder =
          encoding === "br" ? zlib.createBrotliDecompress()
          : encoding === "gzip" ? zlib.createGunzip()
          : null;
        const stream = decoder ? res.pipe(decoder) : res;
        const chunks = [];
        stream.on("data", (chunk) => chunks.push(chunk));
        stream.on("end", () =>
          resolve({ status: res.statusCode, etag: res.headers.etag, body: JSON.parse(Buffer.concat(chunks)) }),
        );
        stream.on("error", reject);
      })
      .on("error", reject);
  });

const url = "https://tokens.swapkit.dev/tokens.json";
const first = await getJson(url);
const again = await getJson(url, { "If-None-Match": first.etag });
console.log(first.body.tokens.length, again.status); // …, 304
```

{% endtab %}

{% tab title="Python (requests)" %}

```python
# pip install requests brotli   — brotli is optional; without it requests uses gzip.
import requests

url = "https://tokens.swapkit.dev/tokens.json"

res = requests.get(url)            # sends Accept-Encoding and decompresses for you
tokens = res.json()["tokens"]
print(res.headers.get("Content-Encoding"))   # "br" with brotli installed, else "gzip"

again = requests.get(url, headers={"If-None-Match": res.headers["ETag"]})
print(again.status_code)           # 304 while the file is unchanged
```

{% endtab %}

{% tab title="Python (stdlib)" %}

```python
import gzip
import json
import urllib.error
import urllib.request

url = "https://tokens.swapkit.dev/tokens.json"

# urllib does not decompress: ask for gzip and unwrap it yourself.
req = urllib.request.Request(url, headers={"Accept-Encoding": "gzip"})
with urllib.request.urlopen(req) as res:
    raw = res.read()
    body = gzip.decompress(raw) if res.headers.get("Content-Encoding") == "gzip" else raw
    tokens = json.loads(body)["tokens"]
    etag = res.headers["ETag"]

# urllib raises on 304, so catch it.
req = urllib.request.Request(url, headers={"Accept-Encoding": "gzip", "If-None-Match": etag})
try:
    urllib.request.urlopen(req)
except urllib.error.HTTPError as err:
    print(err.code)                # 304 while the file is unchanged
```

{% endtab %}

{% tab title="Go" %}

```go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

func main() {
	url := "https://tokens.swapkit.dev/tokens.json"

	// Don't set Accept-Encoding: net/http then requests gzip and decompresses for you.
	// Setting the header yourself switches that off and hands you the compressed bytes.
	res, err := http.Get(url)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var file struct {
		Tokens []json.RawMessage `json:"tokens"`
	}
	if err := json.NewDecoder(res.Body).Decode(&file); err != nil {
		panic(err)
	}
	etag := res.Header.Get("ETag")
	fmt.Println(len(file.Tokens), res.Uncompressed) // …, true — it arrived gzipped

	req, _ := http.NewRequest(http.MethodGet, url, nil)
	req.Header.Set("If-None-Match", etag)
	again, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	again.Body.Close()
	fmt.Println(again.StatusCode) // 304 while the file is unchanged
}
```

{% endtab %}

{% tab title="Go (brotli)" %}

```go
// go get github.com/andybalholm/brotli
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/andybalholm/brotli"
)

func main() {
	url := "https://tokens.swapkit.dev/paths/1.json"

	// Setting Accept-Encoding yourself turns off net/http's transparent gzip,
	// so decode the body according to Content-Encoding.
	req, _ := http.NewRequest(http.MethodGet, url, nil)
	req.Header.Set("Accept-Encoding", "br")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var body io.Reader = res.Body
	if res.Header.Get("Content-Encoding") == "br" {
		body = brotli.NewReader(res.Body)
	}
	var file struct {
		Paths []json.RawMessage `json:"paths"`
	}
	if err := json.NewDecoder(body).Decode(&file); err != nil {
		panic(err)
	}
	fmt.Println(res.Header.Get("Content-Encoding"), len(file.Paths)) // br …
}
```

{% endtab %}

{% tab title="Java" %}

```java
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.zip.GZIPInputStream;

public class Sync {
  public static void main(String[] args) throws Exception {
    HttpClient client = HttpClient.newHttpClient();
    String url = "https://tokens.swapkit.dev/tokens.json";

    // java.net.http.HttpClient never decompresses: ask for gzip and unwrap it yourself.
    HttpRequest request = HttpRequest.newBuilder(URI.create(url))
        .header("Accept-Encoding", "gzip")
        .build();
    HttpResponse<InputStream> res = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
    String encoding = res.headers().firstValue("Content-Encoding").orElse("");
    String json;
    try (InputStream body = "gzip".equals(encoding) ? new GZIPInputStream(res.body()) : res.body()) {
      json = new String(body.readAllBytes());
    }
    String etag = res.headers().firstValue("ETag").orElseThrow();

    HttpRequest conditional = HttpRequest.newBuilder(URI.create(url))
        .header("Accept-Encoding", "gzip")
        .header("If-None-Match", etag)
        .build();
    System.out.println(client.send(conditional, HttpResponse.BodyHandlers.discarding()).statusCode()); // 304
  }
}
```

{% endtab %}

{% tab title="cURL" %}

```bash
# --compressed sends Accept-Encoding and decompresses the output.
curl -s --compressed -D tokens.headers -o tokens.json https://tokens.swapkit.dev/tokens.json
ETAG=$(awk -F': ' 'tolower($1)=="etag" {print $2}' tokens.headers | tr -d '\r')

curl -s --compressed -o /dev/null -w "%{http_code}\n" \
  -H "If-None-Match: $ETAG" https://tokens.swapkit.dev/tokens.json   # 304
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
**Don't decompress twice.** If your client already decompresses (the "Yes" rows above), the body you read is plain JSON — running it through gzip again fails. And **don't send `Accept-Encoding` without decompressing**: you'll receive compressed bytes and `JSON.parse` will fail on them.
{% endhint %}

### File sizes

Measured on 2026-10-07. Sizes move as tokens and routes change, so don't hard-code limits from them.

| File                               | Uncompressed | brotli | gzip   |
| ---------------------------------- | ------------ | ------ | ------ |
| `tokens.json`                      | 1.2 MB       | 208 KB | 218 KB |
| `networks.json`                    | 3.4 KB       | 1 KB   | 1 KB   |
| `paths/1.json` (Ethereum, largest) | 773 KB       | 44 KB  | 400 KB |
| `paths/56.json` (BSC)              | 267 KB       | 22 KB  | 118 KB |
| A chain with a handful of tokens   | 1–35 KB      | 1–7 KB | 1–7 KB |
| **All 35 paths files together**    | 2.3 MB       | 281 KB | 786 KB |

A first sync is about 490 KB with brotli (about 1 MB with gzip); every sync after that is a few dozen `304`s plus whichever files changed.

***

## `tokens.json`

**URL:** `https://tokens.swapkit.dev/tokens.json`

```json
{
  "version": { "major": 1, "minor": 0, "patch": 0 },
  "timestamp": "2026-10-06T16:04:48.283Z",
  "count": 3412,
  "tokens": [
    {
      "identifier": "BTC.BTC",
      "chain": "BTC",
      "chainId": "bitcoin",
      "network": "bitcoin",
      "contractAddress": null,
      "decimals": 8,
      "symbol": "BTC",
      "name": "Bitcoin",
      "logoURI": "https://tokens.swapkit.dev/images/btc.btc.png",
      "coingeckoId": "bitcoin"
    },
    {
      "identifier": "ETH.USDC-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "chain": "ETH",
      "chainId": "1",
      "network": "ethereum",
      "contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "decimals": 6,
      "symbol": "USDC",
      "name": "USDC",
      "logoURI": "https://tokens.swapkit.dev/images/eth.usdc-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png",
      "coingeckoId": "usd-coin"
    }
  ]
}
```

<table><thead><tr><th width="190">Field</th><th width="110">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>identifier</code></td><td><code>string</code></td><td>The SwapKit identifier — the value <a href="/api-reference/v3-quote-request-a-swap-quote.md"><code>/v3/quote</code></a> takes as <code>sellAsset</code> / <code>buyAsset</code>, and the value the paths files use.</td></tr><tr><td><code>chain</code></td><td><code>string</code></td><td>SwapKit chain code, the prefix of the identifier (<code>ETH</code>, <code>BTC</code>, <code>SOL</code>).</td></tr><tr><td><code>chainId</code></td><td><code>string</code></td><td>SwapKit chain ID: a numeric ID for EVM chains (<code>1</code>, <code>42161</code>) or a name (<code>bitcoin</code>, <code>solana</code>).</td></tr><tr><td><code>network</code></td><td><code>string</code></td><td>The chain's CoinGecko asset-platform ID (<code>ethereum</code>, <code>binance-smart-chain</code>). For chains CoinGecko has no asset platform for — Bitcoin, Litecoin and other native-coin-only chains, plus a few newer chains such as Arc (<code>arc</code>) and Robinhood Chain (<code>robinhood</code>) — the CoinGecko ID of the chain's native coin.</td></tr><tr><td><code>contractAddress</code></td><td><code>string | null</code></td><td>The token's contract, mint or account address. <code>null</code> for a chain's native coin — always present, never omitted.</td></tr><tr><td><code>decimals</code></td><td><code>number</code></td><td>The token's decimals.</td></tr><tr><td><code>symbol</code></td><td><code>string</code></td><td>The token's ticker (<code>USDC</code>) — not the <code>symbol</code> field of <code>/tokens</code>, which includes the address.</td></tr><tr><td><code>name</code>, <code>logoURI</code>, <code>coingeckoId</code></td><td><code>string</code></td><td>Display metadata. <em>Optional — omitted when unknown.</em></td></tr><tr><td><code>geoBlockRule</code></td><td><code>string</code></td><td>Present only when a jurisdiction restriction applies — see <a href="/guides/tokenized-equities-and-geo-restrictions.md">Tokenized equities &#x26; geo-restrictions</a>.</td></tr></tbody></table>

`tokens.json` lists the same tokens as [`GET /tokens?provider=all`](/api-reference/tokens-list-and-search-supported-tokens.md), one entry per identifier, sorted by identifier (case-insensitive).

***

## `networks.json`

**URL:** `https://tokens.swapkit.dev/networks.json`

```json
{
  "version": { "major": 1, "minor": 0, "patch": 0 },
  "timestamp": "2026-10-06T16:04:48.283Z",
  "count": 35,
  "networks": [
    { "chainId": "1", "chain": "ETH", "network": "ethereum", "nativeToken": "ETH.ETH", "tokens": 1550 },
    { "chainId": "42161", "chain": "ARB", "network": "arbitrum-one", "nativeToken": "ARB.ETH", "tokens": 313 },
    { "chainId": "bitcoin", "chain": "BTC", "network": "bitcoin", "nativeToken": "BTC.BTC", "tokens": 1 }
  ]
}
```

<table><thead><tr><th width="190">Field</th><th width="110">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>chainId</code></td><td><code>string</code></td><td>SwapKit chain ID. Also the name of the chain's paths file: <code>paths/{chainId}.json</code>.</td></tr><tr><td><code>chain</code></td><td><code>string</code></td><td>SwapKit chain code.</td></tr><tr><td><code>network</code></td><td><code>string</code></td><td>Same as <code>network</code> in <code>tokens.json</code>.</td></tr><tr><td><code>nativeToken</code></td><td><code>string | null</code></td><td>The identifier of the chain's native token, spelled as in <code>tokens.json</code>. <code>null</code> when the catalogue doesn't list it.</td></tr><tr><td><code>tokens</code></td><td><code>number</code></td><td>How many tokens <code>tokens.json</code> lists on this chain.</td></tr></tbody></table>

Every chain in `networks.json` has tokens in `tokens.json`, and every chain in `tokens.json` appears in `networks.json`.

***

## `paths/{srcChainId}.json`

**URL:** `https://tokens.swapkit.dev/paths/{srcChainId}.json` — for example `paths/1.json` (Ethereum) or `paths/solana.json`.

Most tokens on a chain can be swapped into exactly the same set of tokens, so each file lists every distinct destination list **once** in `dstSets`, and `paths` points each source token at its list. Written out per token instead, a single chain could run to hundreds of megabytes.

```json
{
  "version": { "major": 3, "minor": 0, "patch": 0 },
  "timestamp": "2026-10-07T06:02:02.301Z",
  "srcChainId": "1",
  "count": 1078,
  "dstSets": {
    "d1": ["ADA.ADA", "ARB.ETH", "BTC.BTC", "ETH.ETH", "ETH.USDC-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "SOL.SOL", "…"],
    "d5": ["ADA.ADA", "ARB.ETH", "BTC.BTC", "ETH.LINK-0x514910771AF9Ca656af840dff83E8264EcF986CA", "ETH.USDC-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "SOL.SOL", "…"]
  },
  "paths": [
    { "srcToken": "ETH.ETH", "dstSet": "d5" },
    { "srcToken": "ETH.LINK-0x514910771AF9Ca656af840dff83E8264EcF986CA", "dstSet": "d1" }
  ]
}
```

Trimmed from the live `paths/1.json` — the real lists hold about 1,300 identifiers each. List names change between versions.

<table><thead><tr><th width="190">Field</th><th width="110">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>srcChainId</code></td><td><code>string</code></td><td>The chain this file covers. Every <code>srcToken</code> is on this chain.</td></tr><tr><td><code>count</code></td><td><code>number</code></td><td>Number of entries in <code>paths</code>.</td></tr><tr><td><code>dstSets</code></td><td><code>object</code></td><td>Destination lists by name. Each list holds SwapKit identifiers, on any chain, sorted case-insensitively.</td></tr><tr><td><code>paths[].srcToken</code></td><td><code>string</code></td><td>A token on <code>srcChainId</code> that SwapKit can swap from.</td></tr><tr><td><code>paths[].dstSet</code></td><td><code>string</code></td><td>The name of the <code>dstSets</code> list this token can be swapped into.</td></tr></tbody></table>

{% hint style="warning" %}
**`paths[].dst` was renamed to `paths[].dstSet`** in API release v3.44.0. The first paths file published after that release carries a **major** version bump on every chain, even where no route changed. Read `dstSet`; files no longer contain `dst`.
{% endhint %}

Rules for reading a paths file:

* **Skip the token itself.** A list may contain the source token — that is what lets many tokens share one list. Treat `srcToken == destination` as not a route.
* **List names are local.** `d0` in `paths/1.json` is unrelated to `d0` in `paths/solana.json`, and names can change between versions. Resolve them when you load a file; don't store them.
* **A token missing from `paths` has no route** from this chain today. Chains with no routable tokens still have a file, with an empty `paths`.

***

## Identifiers

Every file uses **SwapKit identifiers**, the same strings `/v3/quote` takes, so a route from these files can be quoted without translation.

* The part before the first `.` is the chain code: `ETH.USDC-0x…`, `SOL.USDC-EPjF…`, `NEAR.wBTC-2260…factory.bridge.near`. Native coins have no address part: `BTC.BTC`, `ETH.ETH`.
* Every identifier a paths file names comes from the `tokens.json` published when that paths file was built. Paths files are rebuilt on a shorter cycle than `tokens.json` (see [Freshness](#freshness-and-versioning)), so after `tokens.json` changes they catch up within one cycle.
* **Match identifiers exactly as published** — every file spells a token the same way. Addresses on Solana, Tron, TON and other non-EVM chains are case-sensitive.
* To map an identifier to your own `chainId + address` key, use `chainId` and `contractAddress` from `tokens.json`.

***

## Freshness and versioning

* **Cadence.** `tokens.json` and `networks.json` are rebuilt every 8 hours, the paths files every 6 hours. A file is rewritten only when its content changes, so its `ETag` and `Last-Modified` stay the same otherwise.
* **Caching.** Files are served with `Cache-Control: public, max-age=300`, so a change can take up to five minutes to reach you after it's written. Polling every **15 minutes** with conditional requests is plenty; almost every request is an empty `304`.
* **`version`** follows token-list semver:

| Bump      | When                                                                                                                                         |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| **major** | Something you may have stored is no longer valid: a token or route removed, or a token's `chainId`, `contractAddress` or `decimals` changed. |
| **minor** | Tokens, chains or routes added.                                                                                                              |
| **patch** | Metadata only — a name, a logo, a `network` value.                                                                                           |

`timestamp` is when the file's content last changed.

**Expect paths files to change more often than `tokens.json`.** Which routes are available moves with provider status and liquidity, so a paths file can gain and lose destinations between rebuilds, and a lost destination is a **major** bump. Treat any new version of a paths file as a replacement: swap in the whole file rather than merging it into what you had, and don't alert on its major bumps. A major bump on `tokens.json` is rarer and worth reviewing: a token was removed, or its contract or decimals changed.

***

## Example: sync loop

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const BASE = "https://tokens.swapkit.dev";
const etags = new Map<string, string>();
const bodies = new Map<string, any>();

async function getJson(path: string) {
  const url = `${BASE}${path}`;
  const etag = etags.get(url);
  const res = await fetch(url, { headers: etag ? { "If-None-Match": etag } : {} });
  if (res.status === 304) return bodies.get(url); // unchanged since the last sync
  if (!res.ok) throw new Error(`${url} → ${res.status}`);
  const body = await res.json();
  const newEtag = res.headers.get("ETag");
  if (newEtag) etags.set(url, newEtag);
  bodies.set(url, body);
  return body;
}

async function sync() {
  const { tokens } = await getJson("/tokens.json");
  const tokenByIdentifier = new Map(tokens.map((t: any) => [t.identifier, t]));

  const { networks } = await getJson("/networks.json");
  const routes = new Map<string, Set<string>>(); // sell identifier → buy identifiers

  for (const { chainId } of networks) {
    const { dstSets, paths } = await getJson(`/paths/${chainId}.json`);
    const sets = Object.fromEntries(
      Object.entries(dstSets).map(([name, ids]) => [name, new Set(ids as string[])]),
    );
    // Each source token references a shared Set — memory stays close to the file size.
    for (const { srcToken, dstSet } of paths) routes.set(srcToken, sets[dstSet]!);
  }

  return { tokenByIdentifier, routes };
}

// Can SELL be swapped into BUY? Both are SwapKit identifiers, ready for /v3/quote.
const canSwap = (routes: Map<string, Set<string>>, sell: string, buy: string) =>
  sell !== buy && (routes.get(sell)?.has(buy) ?? false);
```

{% endtab %}
{% endtabs %}

***

## What the files don't tell you

A route listed in these files is one SwapKit can quote. A particular request can still fail for reasons only known at quote time:

* **Amount limits** — minimums, maximums and per-route size ceilings. See [Route availability and providers errors](/api-reference/route-availability-and-size-limits.md).
* **Liquidity** — the route exists but can't fill the amount: `insufficientLiquidity`, or no route for that size.
* **Regional restrictions** — some tokenized equities are rejected with `451` for callers in restricted jurisdictions. The files list every token for every caller; use `geoBlockRule`. See [Tokenized equities & geo-restrictions](/guides/tokenized-equities-and-geo-restrictions.md).
* **Your API key's settings** — the files reflect SwapKit's global provider and chain availability. If a provider or chain is switched off for your key only, the files still list its routes.

These surface as normal `/v3/quote` responses, so handle them the way you already do.
