DEV Community

VASP Tracker
VASP Tracker

Posted on

Brazil now lists who applied for a crypto licence. Reading the two BCB lists in Python, and the CNPJ trap between them

Brazil's central bank (Banco Central do Brasil, BCB) now publishes two lists that crypto compliance teams have been waiting for:

  1. Companies that applied for authorisation as virtual asset service providers (SPSAVs) under the transition rule in article 88 of Resolução BCB nº 520.
  2. Institutions the BCB already authorises (banks, brokers, distributors) that told it they will offer virtual asset services under article 22 of the same resolution.

Both sit on the BCB's authorisation page. The page is rendered from two JSON endpoints, so you can read them directly. This post shows a short script, the four things that tripped me up, and why the lists matter this month.

All numbers come from the endpoints as I read them on 10 November 2026, around 10:05 Asia/Shanghai time (UTC+8). The lists will change, so re-run the code before you quote anything.

The endpoints

https://www.bcb.gov.br/api/servico/sitebcb/SPSVAs_autorizacao
https://www.bcb.gov.br/api/servico/sitebcb/ifs-autorizadas-ativos-virtuais
Enter fullscreen mode Exit fullscreen mode

No key, no login. Each returns an object with a single conteudo array. These endpoints feed a public web page; they are not a documented open-data API. So cache the result, poll politely, and expect the shape to change without notice.

The script

import re
import unicodedata
from collections import Counter
import requests

BASE = "https://www.bcb.gov.br/api/servico/sitebcb/"

def fetch(endpoint: str) -> list[dict]:
    r = requests.get(BASE + endpoint, timeout=30)
    r.raise_for_status()
    return r.json()["conteudo"]

applicants = fetch("SPSVAs_autorizacao")              # art. 88 applications
incumbents = fetch("ifs-autorizadas-ativos-virtuais")  # art. 22 institutions

print(len(applicants), "applications |", len(incumbents), "art. 22 institutions")
print(Counter(a["Modalidade"] for a in applicants))
print(Counter(a["Fase"] for a in applicants))
print(Counter(i["TipoAtuacao"] for i in incumbents))
Enter fullscreen mode Exit fullscreen mode

Output on 10 November:

120 applications | 24 art. 22 institutions
Counter({'Intermediária': 73, 'Corretora': 46, 'Custodiante': 1})
Counter({'Em exame': 119, 'Indeferido': 1})
Counter({'Intermediação': 16, 'Custódia/Intermediação': 8})
Enter fullscreen mode Exit fullscreen mode

So 120 applications: 73 for the intermediary modality, 46 for broker and one for custodian. 119 are "em exame" (under examination) and one is "indeferido" (rejected). On the second list, 8 institutions are shown for custody and intermediation and 16 for intermediation only.

Four traps

1. The two lists format the company ID differently

Brazilian companies are identified by a CNPJ. The first eight digits (the raiz, or root) identify the company; the rest identify the branch. Both lists give only the root, but not in the same format:

print(Counter(len(a["CNPJ"]) for a in applicants))   # Counter({10: 120})  e.g. "39.695.312"
print(Counter(len(i["CNPJ"]) for i in incumbents))   # Counter({8: 24})    e.g. "60701190"
Enter fullscreen mode Exit fullscreen mode

The applications list uses dots; the article 22 list uses bare digits. A naive join on the CNPJ field returns zero matches, and you will not get an error. Normalise to eight digits first:

def cnpj_root(value: str) -> str:
    digits = re.sub(r"\D", "", value)
    if len(digits) == 14:          # a full CNPJ, e.g. from your own KYC data
        digits = digits[:8]
    return digits.zfill(8)
Enter fullscreen mode Exit fullscreen mode

The len == 14 branch matters because your own records probably hold full CNPJs (60.701.190/0001-04), not roots.

2. Leading zeros are real

The article 22 list includes roots that start with zero. One of them is 00000000, which is Banco do Brasil. The JSON keeps them as strings, so you are fine until you save to CSV and read it back:

import pandas as pd

pd.read_csv("roots.csv")["cnpj"].tolist()             # [0, 9516419]
pd.read_csv("roots.csv", dtype=str)["cnpj"].tolist()  # ['00000000', '09516419']
Enter fullscreen mode Exit fullscreen mode

Always pass dtype=str for ID columns, and keep the zfill(8) in cnpj_root as a second line of defence.

3. Names have stray whitespace

Four of the 120 company names contain a double space or a trailing tab, for example 'WEBULL Sociedade Prestadora de Serviços de Ativos Virtuais Ltda.\t'. If you compare names, collapse whitespace first:

def clean_name(value: str) -> str:
    return re.sub(r"\s+", " ", value).strip()
Enter fullscreen mode Exit fullscreen mode

4. You can't tell an SPSAV by its name

Article 15 of Resolution 520 requires an SPSAV to carry the expression "Sociedade Prestadora de Serviços de Ativos Virtuais" in its company name, and most applicants already do. But matching on the phrase gives different answers depending on how strict you are:

def fold(value: str) -> str:
    s = unicodedata.normalize("NFKD", clean_name(value)).casefold()
    return "".join(c for c in s if not unicodedata.combining(c))

phrase = "Sociedade Prestadora de Serviços de Ativos Virtuais"
exact  = sum(phrase in a["Title"] for a in applicants)
folded = sum(fold(phrase) in fold(a["Title"]) for a in applicants)
print(exact, folded)   # 99 104
Enter fullscreen mode Exit fullscreen mode

The five names that only the folded match finds are two written in capitals, two without the cedilla ("Servicos") and one with the double space from trap 3. The other 16 applicants are listed under names without the phrase at all ("... Serviços Digitais Ltda.", "... Securitizadora S.A." and similar). Do not use the phrase as a filter. Treat the list as the source of truth, keyed by CNPJ root.

A lookup you can put in front of onboarding

index = {}
for a in applicants:
    index[cnpj_root(a["CNPJ"])] = ("art. 88 application", a["Modalidade"], a["Fase"], clean_name(a["Title"]))
for i in incumbents:
    index[cnpj_root(i["CNPJ"])] = ("art. 22 institution", i["TipoAtuacao"], None, clean_name(i["Title"]))

def lookup(cnpj: str):
    return index.get(cnpj_root(cnpj), "not on either list")

print(len(index))                       # 144: no root appears on both lists
print(lookup("60.701.190/0001-04"))     # ('art. 22 institution', 'Custódia/Intermediação', None, 'ITAÚ UNIBANCO S.A.')
Enter fullscreen mode Exit fullscreen mode

A few rules for using the result:

  • "Em exame" is not a licence. It means an application is on file and the process has not been concluded. Resolution 520 treats the two differently: an on-time applicant may keep operating "until the conclusion of its authorisation process" (art. 88, para. 6), which is not the same as being authorised.
  • "Indeferido" needs a human. The list gives no reasons.
  • "Not on either list" is not proof of anything on its own. Check the spelling of the CNPJ, re-fetch, and look at other BCB sources before drawing a conclusion.
  • Store the snapshot with a timestamp. If a decision is ever questioned, you want to show what the list said on the day you checked.

Why this matters in November

Two dates in Resolution 520 make these lists practical tools rather than trivia:

  • Article 91, as reworded by Resolução BCB nº 589, has applied since 6 November 2026. BCB-authorised institutions may not carry out or enable virtual asset operations whose counterparties are virtual asset service providers not authorised by the BCB, except in forms the regulation expressly allows. In its Vote 108/2026, the BCB said it moved this date from 30 October so it could publish an official list of institutions in transition.
  • Article 88, paragraph 7: providers that did not apply by the 30 October deadline must stop providing virtual asset services within 30 days after it. By our count that is 29 November 2026. Under paragraph 6, providers that applied on time may keep operating until their process is concluded, but may not switch modality in the meantime.

If you build payment, banking or exchange tooling for Brazil, the CNPJ lookup above is the cheapest control you can add before 29 November. For the wider timeline, VASP Tracker keeps a Brazil page that follows the same BCB sources, also in Portuguese.

Official texts: Resolução BCB nº 520, Resolução BCB nº 589, Voto 108/2026.


This is not legal advice. The BCB's own pages and the resolutions are the authoritative sources; check them before relying on anything here. VASP Tracker is an independent tracker, not an official source, and is not affiliated with any regulator.

Top comments (0)

⌂ 换网址