kazna-catalog

Payee and bank packs with logos for Kazna.

Served by GitHub Pages at https://catalog.kazna.app, straight from this repository: a push to main is live within minutes, and what isn’t committed isn’t published.

Two formats from one source

You edit packs/ and meta/. The app reads them in two forms:

Layout

The layout is flat: one folder per pack, edited in place, with no version folders. Pack and logo URLs therefore stay the same for good, and a pack’s version tells installed apps that it changed.

catalog.json                 v1 list of every pack (edited by hand)
packs/{id}/
  pack.json                  the pack itself: what the app installs
  logos/{key}.png            one logo per payee or bank, named by its key
  sources.json               where each logo came from (for us; the app never reads it)
meta/                        what the v2 index adds to the packs (edited by hand)
  categories.json            brand categories
  packs.json                 every pack's rank, countries and default category
  entities.json              overrides by key
  regions.json               reserve currencies and short country names
  renamed.json               old key -> the key that replaced it
index/v2/                    GENERATED by build.py and committed; never edit it by hand
  banks.json
  brands.json
  logos/{h}.png              a copy of every logo the index uses, named by its hash; only ever added to
schema/
  catalog.schema.json        the shape of catalog.json
  pack.schema.json           the shape of packs/{id}/pack.json
validate.py                  checks packs/ and catalog.json
build.py                     writes index/v2/ from packs/ and meta/; --check verifies it
.github/workflows/check.yml  runs both checks on every push and pull request

Every change

  1. Edit packs/ or meta/, and catalog.json if a pack’s listing changed.
  2. Run python3 build.py. It must run after every change to packs/ or meta/, even a logo-only fix: it rewrites index/v2/banks.json and brands.json and adds the new logo copies.
  3. Run python3 validate.py and python3 build.py --check. Before a push, add --base origin/main to both, so they compare with what is published.
  4. Commit packs/, meta/, catalog.json and index/v2/ together, and push to main.

Build on the day you commit. The index’s updated is the date of the last commit that touched a pack.json, a pack’s logos or meta/, so an index built the day before reads as stale.

CI (.github/workflows/check.yml) runs validate.py and build.py --check on every push and pull request, both compared with the commit before the push or with the pull request’s base. It fails when index/v2/ wasn’t rebuilt and committed.

Both scripts use only the Python standard library, so they need no venv.

catalog.json (v1)

Top level: schema_version (1), generated_at (the date of the last edit), base_url (https://catalog.kazna.app, which the app also has built in) and packs.

Each entry in packs:

Field Description
id The pack’s folder name, kebab-case
type payees or banks
version The same number as in pack.json
name, description Localized text (see below)
tags Kebab-case labels. The app stores them but doesn’t show them yet.
entity_count How many payees or banks pack.json has
pack_url {base_url}/packs/{id}/pack.json
preview_logos Up to 4 logo URLs of the pack’s own payees or banks, shown on payee pack cards before install

Localized text is an object keyed by language code, such as {"en": "Russian Supermarkets", "ru": "Супермаркеты"}. en is required, and the app falls back to it. Any code is allowed. Builds up to 1.12.15 pick the device’s language, and later ones pick one of en, ru, de, es and fr.

pack.json

{
  "id": "ru-supermarkets", "version": 3, "type": "payees",
  "name": { "en": "Russian Supermarkets", "ru": "Супермаркеты" },
  "payees": [
    { "key": "pyaterochka", "name": { "en": "Pyaterochka", "ru": "Пятёрочка" },
      "logo": "https://catalog.kazna.app/packs/ru-supermarkets/logos/pyaterochka.png",
      "mcc": [5411], "website": "https://5ka.ru", "aliases": ["5ka"] }
  ]
}

A payee pack has payees and a bank pack has banks, never both. A pack’s rank, countries and default category are not in pack.json but in meta/packs.json.

Payee:

Field Description Read by
key, name, logo Required every build
mcc Merchant category codes, 1–9999 the v2 index only (builds parse it from pack.json but never use it)
website The brand’s site the v2 index only (likewise)
aliases Other names people search by: short forms, other scripts, misspellings. A brand’s earlier names go in former in meta/entities.json instead. the v2 index only
countries ISO codes, or null for international; replaces the countries of the packs that list the key the v2 index only
category A category id from meta/categories.json, in place of the pack’s default the v2 index only
match "pick" when the name is too generic (МЕТРО, Claude): the app suggests the brand but never links a payee of the user’s to it by name alone install and update, from 1.12.17

Bank:

{ "key": "tbc-ge", "name": { "en": "TBC Bank", "ru": "ТБС Банк" },
  "logo": "https://catalog.kazna.app/packs/ge-banks/logos/tbc-ge.png",
  "countries": ["GE"], "website": "https://tbcbank.ge",
  "card_background": "solid:#00ADEE", "accent_color": "#00ADEE", "brand": "tbc" }
Field Description
key, name Required
logo Optional. A bank without one (a closed bank, say) is shown by its initial.
countries ISO 3166-1 alpha-2 codes, or null for an international bank such as Revolut
website The bank’s site. No build reads it yet.
card_background solid:#RRGGBB, the colour of the bank’s cards
accent_color #RRGGBB
closed true for a bank that no longer operates
brand The group this entry is one country of. The app shows each group as one bank.
aliases Other names people search by (Тинькофф for T-Bank). Read by the v2 index only.

meta/entities.json overrides aliases, countries, category and name (see meta/), in the v2 index only.

Keys

Versions

Logos

meta/

What the v2 index adds to the packs. It overrides anything a pack.json says, and changing it never needs a version bump.

File Holds
meta/categories.json The brand categories: id, rank, icon (a short id the app maps to its own glyph), color (#RRGGBB) and n, names in en, ru, de, es and fr.
meta/packs.json Every pack: rank, countries ([] means international) and, for payee packs, the category its entries get unless they say otherwise. The build fails on a pack that is missing.
meta/entities.json Overrides by key: category, countries, aliases, pick: true (suggest, never link by name), full_bleed: true (a tile the app can’t detect), and the names below: name, former, translit: true. An override for a key no pack has fails the build. A key that is both a bank and a brand (paypal) gets the override in both files, except former and translit, which are for brands only.
meta/regions.json reserve currencies, currency → country regions overrides, and short country_names.
meta/renamed.json Old key → the key that replaced it, a key of the same kind. Entries are never removed.

Names

A name in the index is for good. Search finds a brand by every name it ever had, and the app tells a pack’s untouched payee from one the user renamed by comparing its title with the brand’s names, so build.py --check fails when a name published in the committed index (any language’s n, and a brand’s o and r) is gone from its entry, or from the entry of the key that replaced it: a brand must keep it in n, a, o or r, a bank in n or a. Aliases alone may go. It warns, though, when a bank alias published there is gone, since an alias is all that keeps a bank’s old name.

Rename in meta/entities.json, not in pack.json. name is merged over the pack’s names by language, in the index only: pack.json keeps its names, so builds up to 1.12.17 see no change and no version bump is needed. Every pack name it replaces lands in the brand’s r by itself, so «Эппл» needs no alias to stay searchable.

"sas":             { "name": { "ru": "Скандинавские Авиалинии" } },
"zolotaya-korona": { "name": { "en": "Zolotaya Korona" } },
"apple":           { "name": { "ru": "Apple" }, "translit": true },
"kuper":           { "former": ["СберМаркет", "SberMarket"] }

Edit a name in pack.json only for something builds up to 1.12.17 must see as well. That needs a version bump (see Versions), and the old name has to stay in aliases or former.

Index v2

index/v2/banks.json and index/v2/brands.json hold every bank and every brand, with short field names to keep them small. The app parses them leniently, ignoring fields it doesn’t know. Change the meaning of a field only together with the app.

Both files start with:

Field Meaning
format 2. The app ignores a file with any other format.
rev The content’s revision: the first 16 hex digits of HMAC-SHA256 with the key kazna-index-v1 over the file’s canonical JSON (sorted keys, no spaces, UTF-8) without rev and updated. Use this, not the ETag, to tell whether the file changed: Pages changes every ETag on every deploy.
updated The date of the last commit that touched a pack.json, a pack’s logos or meta/
min_app_build The oldest app version that may read the file, as major × 1,000,000 + minor × 1,000 + patch of the version both platforms share (1.13.0 = 1013000), or 0 for any. Not a store build number: every build that reads index v2 counts as 1012018 or more, so 66 would turn no one away, and build.py fails on anything between 1 and 999,999. Older builds ignore the file and keep what they have. It is MIN_APP_BUILD in build.py.
base_url, logo_path A logo’s URL is base_url + logo_path, with {h} replaced by the entry’s h
renamed Old key → new key, from meta/renamed.json, in the file of the new key’s kind

banks.json also has reserve, regions and country_names from meta/regions.json. brands.json also has categories from meta/categories.json.

Banks, ordered by pack rank, then by their order in the pack, which is the curated popularity order:

Field Meaning From
k Key key
p Pack the folder
b Brand group, only on entries whose key isn’t the group’s; the app groups by b, else k brand
n Names by language name, with name in meta/entities.json merged over it
a Aliases meta/entities.json, else aliases
c Countries; absent means international meta/entities.json, else countries, else meta/packs.json
h Logo hash; absent means no logo the logo file
f 1: the logo is a full-bleed tile the app can’t detect full_bleed in meta/entities.json
bg Card colour a solid: card_background
ac Accent colour accent_color
x 1: closed closed

Brands, one per payee key, ordered by category rank, then pack rank, then their order in the pack:

Field Meaning From
k, n, a, h, f As for banks the same
o Former names: what the brand went by before a rebrand. Searched, shown with the brand former in meta/entities.json
r Replaced names: every pack name a name override replaced and the brand no longer shows. Searched, never shown worked out by build.py
t Transliterations: the names in r that a translit: true override replaced and that aren’t Latin. No app reads it yet worked out by build.py
p The first pack, in rank order, that lists the key  
ps Every pack that lists the key, when there is more than one  
g Category id; always present meta/entities.json, else category, else the first pack’s category in meta/packs.json
c Countries; absent means international meta/entities.json, else countries, else the union over the packs that list the key (an international pack makes it international, and build.py warns when that takes the countries away from a brand that had them)
m MCC codes mcc
w Website without its scheme or final slash; a path stays (www.tbank.ru/insurance) website
pick 1: suggest, never link by name pick in meta/entities.json, or "match": "pick"

Logos. h is the first 16 hex digits of HMAC-SHA256 with the key kazna-logo-v1 over the PNG’s bytes. The app has the same function, and it drops a logo whose bytes don’t match. build.py copies each logo to index/v2/logos/{h}.png. A fixed logo gets a new hash, so a new URL: the CDN ignores query strings and caches for 10 minutes, so a URL must change whenever its bytes do. These copies are never edited or deleted, only added, because an older index or a snapshot bundled in an app may still point at them. build.py fails when a copy committed at --base is gone, even one nothing references any more.

Size budgets: banks.json at most 120 KB raw or 30 KB gzipped, and brands.json at most 320 KB raw or 70 KB gzipped. build.py warns at 80%.

Checks

python3 validate.py                           # before a commit: compares the packs with HEAD
python3 build.py --check                      # before a commit: index/v2/ is current and meta/ complete
python3 validate.py --base origin/main        # before a push: compares with what is published
python3 build.py --check --base origin/main

Both exit with status 1 on errors and list warnings separately.

validate.py checks the packs and catalog.json. Its errors:

Its warnings:

Without git, or with a --base that isn’t a commit, the version and key checks are skipped with a warning. The schemas are checked by a small built-in JSON Schema subset. A schema keyword it doesn’t implement stops the run (exit status 2), so a schema edit can’t silently go unchecked.

build.py –check writes nothing. It fails when:

It warns when:

Adding a pack

  1. Create packs/{id}/pack.json and add the logos to packs/{id}/logos/.
  2. Write packs/{id}/sources.json.
  3. Add the pack to catalog.json, with up to 4 preview_logos, and set generated_at to today.
  4. Add it to meta/packs.json with a rank, its countries and, for payees, a category. Give entries that belong elsewhere a category or countries of their own, in pack.json or in meta/entities.json.
  5. Run python3 build.py, then the checks. Commit the pack, catalog.json, meta/ and index/v2/ together, and push to main.

A new country’s bank pack installs itself for people in that country only after an app release maps the country’s currency and seeds the pack. Until then, people add it from Banks → Bank Catalog.

Updating a pack

  1. Edit pack.json, or replace logos in place under the same file names.
  2. If something builds up to 1.12.17 read changed (see Versions), bump version in both pack.json and catalog.json. Update entity_count, description and preview_logos if they changed, and set generated_at to today.
  3. If you removed or renamed a key, add "old-key": "new-key" to meta/renamed.json.
  4. Run python3 build.py, then the checks. Commit the pack, catalog.json, meta/ and index/v2/ together, and push to main.