<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[VASP Tracker]]></title><description><![CDATA[VASP Tracker]]></description><link>https://vasptracker.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 25 Oct 2026 20:09:32 GMT</lastBuildDate><atom:link href="https://vasptracker.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Counterparty Licence Check for Brazil's 6 November Crypto Rule]]></title><description><![CDATA[A compliance-engineering walkthrough with the Central Bank's open authorisation API: data model, evidence trail, and the five traps in the feed]]></description><link>https://vasptracker.hashnode.dev/building-a-counterparty-licence-check-for-brazil-s-6-november-crypto-rule</link><guid isPermaLink="true">https://vasptracker.hashnode.dev/building-a-counterparty-licence-check-for-brazil-s-6-november-crypto-rule</guid><category><![CDATA[fintech]]></category><category><![CDATA[compliance ]]></category><category><![CDATA[Python]]></category><category><![CDATA[api]]></category><category><![CDATA[RegTech]]></category><dc:creator><![CDATA[VASP Tracker]]></dc:creator><pubDate>Wed, 21 Oct 2026 19:48:44 GMT</pubDate><content:encoded><![CDATA[<p>By Daniel Bradley</p>
<p>If you build payment, banking or on-ramp software that touches Brazil, a requirement has probably landed in your backlog this month: <strong>"flag crypto counterparties that aren't authorised by the Central Bank."</strong></p>
<p>The trigger is Article 91 of Resolution BCB 520, as amended by Resolution BCB 589. From <strong>6 November 2026</strong>, financial institutions, payment institutions and other institutions authorised by Banco Central do Brasil (BCB) may not carry out or enable virtual-asset market operations with counterparties that provide virtual-asset services and are not authorised by the BCB, except in the forms the resolution expressly allows. The rule names trading, intermediation and custody, FX, and opening and maintaining payment accounts used to enable those operations.</p>
<p>Whether a given relationship is caught by that rule is a legal question. This post is about the engineering underneath it: <strong>how do you build a status check that produces evidence your compliance team can actually rely on?</strong> We'll use the BCB's own open data, and I'll show the traps I hit while building VASP Tracker's Brazil pipeline.</p>
<h2>The Source: BCB's Olinda OData Service</h2>
<p>The BCB publishes its authorisation-process data on its open-data portal (licensed ODbL) and serves it through an OData API, <code>Processos_Autorizacao</code>, with two functions:</p>
<table>
<thead>
<tr>
<th>function</th>
<th>parameter</th>
<th>returns</th>
</tr>
</thead>
<tbody><tr>
<td><code>ProcessosAutorizacaoFuncionamentoEmAberto</code></td>
<td><code>dataBase</code> (<code>'YYYY-MM'</code>)</td>
<td>open authorisation processes at a monthly reference date: <code>DataBase</code>, <code>NumeroPE</code>, <code>NomeInteressado</code>, <code>CNPJInteressado</code>, <code>CPFInteressado</code>, <code>DataProtocolo</code>, <code>SegmentoPretendido</code></td>
</tr>
<tr>
<td><code>SituacaoProcessosAutorizacao</code></td>
<td><code>numeroPE</code></td>
<td>current status of one process: <code>Situacao</code>, <code>SubunidadeResponsavel</code></td>
</tr>
</tbody></table>
<p>Crypto firms applying under the new regime show up with <code>SegmentoPretendido = "Sociedade Prestadora de ServiÃ§os de Ativos Virtuais"</code> (SPSAV). At the 30 September 2026 reference date, the feed listed 184 open processes across all segments, 7 of them SPSAV.</p>
<h2>Design Principle: Store Evidence, Not Booleans</h2>
<p>The tempting schema is <code>is_authorised: bool</code>. Don't. For a rule like this, what your compliance team needs is <em>what the regulator's data said, as of when, and where it came from.</em> So the check returns an evidence record:</p>
<ul>
<li><code>state</code>: <code>open_process</code> or <code>not_found</code> (and, once your source covers it, <code>authorised</code>)</li>
<li><code>as_of</code>: the <strong>reference date of the data</strong>, not the timestamp of your API call</li>
<li><code>process</code>, <code>segment</code>, <code>situacao</code>: the BCB's own identifiers and labels, verbatim</li>
</ul>
<p>Keep <code>open_process</code> and <code>authorised</code> as <strong>separate states</strong>, and keep the mapping from state to business action ("block", "review", "allow") in configuration owned by compliance, not in code. This matters here in particular. The original text of Article 91 covered providers "not authorised <strong>or in the process of authorisation</strong>". The version amended by Resolution 589 refers only to providers "not authorised". How "in process" should be treated is exactly the kind of call that belongs to your legal team, and it should be changeable without a deploy.</p>
<h2>The Code</h2>
<p>Standard library only, Python 3.10+.</p>
<pre><code class="language-python">import json, re, time, urllib.parse, urllib.request
from dataclasses import dataclass, asdict
from datetime import datetime

BASE = "https://olinda.bcb.gov.br/olinda/servico/Processos_Autorizacao/versao/v1/odata"
SPSAV = "Sociedade Prestadora de ServiÃ§os de Ativos Virtuais"

def odata(path: str, params: dict) -&gt; list:
    qs = urllib.parse.urlencode(params, safe="@'$()")
    url = f"{BASE}/{path}?{qs}"
    for attempt in range(3):
        try:
            req = urllib.request.Request(url, headers={"User-Agent": "counterparty-check/0.1"})
            with urllib.request.urlopen(req, timeout=60) as r:
                return json.loads(r.read())["value"]
        except Exception:
            if attempt == 2:
                raise
            time.sleep(2 ** attempt)

def snapshot(month: str) -&gt; tuple[str, list]:
    """Open authorisation processes for a reference month ('YYYY-MM').
    Returns (reference date the API actually served, rows)."""
    rows = odata("ProcessosAutorizacaoFuncionamentoEmAberto(dataBase=@dataBase)",
                 {"@dataBase": f"'{month}'", "$format": "json", "$top": "20000"})
    served = {r["DataBase"] for r in rows}
    if len(served) != 1:
        raise ValueError(f"mixed reference dates: {served}")
    as_of = datetime.strptime(served.pop(), "%d/%m/%Y").date().isoformat()
    if not as_of.startswith(month):
        raise ValueError(f"asked for {month}, API served {as_of}: snapshot not published yet?")
    return as_of, rows

def root(cnpj: str) -&gt; str:
    d = re.sub(r"\D", "", cnpj or "")
    return d[:8] if len(d) &gt;= 8 else ""

@dataclass
class Evidence:
    cnpj_root: str
    state: str          # "open_process" | "not_found"
    as_of: str          # reference date of the data, not the date you ran the check
    process: int | None = None
    segment: str | None = None
    situacao: str | None = None
    name: str | None = None

def check(roots: list[str], month: str) -&gt; list[Evidence]:
    as_of, rows = snapshot(month)
    by_root = {}
    for r in rows:
        if root(r["CNPJInteressado"]):
            by_root.setdefault(root(r["CNPJInteressado"]), []).append(r)
    out = []
    for cr in roots:
        hits = [r for r in by_root.get(cr, []) if r["SegmentoPretendido"] == SPSAV]
        if not hits:
            out.append(Evidence(cr, "not_found", as_of))
            continue
        for h in hits:
            sit = odata("SituacaoProcessosAutorizacao(numeroPE=@numeroPE)",
                        {"@numeroPE": str(h["NumeroPE"]), "$format": "json"})
            out.append(Evidence(cr, "open_process", as_of, h["NumeroPE"], h["SegmentoPretendido"],
                                sit[0]["Situacao"] if sit else None,
                                " ".join(h["NomeInteressado"].split())))
    return out

if __name__ == "__main__":
    # one CNPJ root with an open SPSAV process at 30/09/2026, one not in that snapshot
    for e in check(["34882109", "18213434"], "2026-09"):
        print(json.dumps(asdict(e), ensure_ascii=False))
    try:
        snapshot("2026-10")
    except ValueError as err:
        print("guard:", err)
</code></pre>
<p>Output on 22 October 2026:</p>
<pre><code class="language-text">{"cnpj_root": "34882109", "state": "open_process", "as_of": "2026-09-30", "process": 315453, "segment": "Sociedade Prestadora de ServiÃ§os de Ativos Virtuais", "situacao": "Em Exame", "name": "TRANSFERO SOCIEDADE PRESTADORA DE SERVICOS DE ATIVOS VIRTUAIS (SPSAV) LTDA."}
{"cnpj_root": "18213434", "state": "not_found", "as_of": "2026-09-30", "process": null, "segment": null, "situacao": null, "name": null}
guard: asked for 2026-10, API served 2026-09-30: snapshot not published yet?
</code></pre>
<p>(<code>Em Exame</code> means "under examination".)</p>
<h2>Five Traps in the Feed</h2>
<p><strong>1. Asking for a month that doesn't exist yet still returns data.</strong> Requesting <code>dataBase='2026-10'</code> (no October snapshot published yet) returned rows labelled <code>30/09/2026</code>. It wasn't even the same set: 186 rows, against 184 for an explicit <code>'2026-09'</code> request. If you key your storage on the month you <em>asked for</em>, you'll silently save September's data as October's. Always key snapshots on the <code>DataBase</code> value in the response, and reject mismatches, which is what the guard in <code>snapshot()</code> does.</p>
<p><strong>2. The CNPJ is truncated, and often missing.</strong> <code>CNPJInteressado</code> holds only the 8-digit company root (for example <code>34.882.109</code>), not the full 14-digit CNPJ. In the 186-row response, 60 rows had no CNPJ at all. Match on the root, and treat a missing root as "cannot match", not "no match".</p>
<p><strong>3. One brand, many legal entities.</strong> Platforms in Brazil often run several entities: an SPSAV, a securities broker, a payment institution. In our Brazil dataset, one large exchange maps to three CNPJ roots. Your counterparty master data has to resolve brand to entity to root before any API call means anything.</p>
<p><strong>4. Fields you'd want are mostly empty.</strong> <code>DataProtocolo</code> (filing date) was filled in for 1 of 186 rows, so you can't use this feed to tell whether a firm filed before 30 October. <code>SegmentoPretendido</code> was blank on 3 rows. Names are free text with stray double spaces. Normalise before you display or fuzzy-match.</p>
<p><strong>5. History has holes.</strong> The August 2024 reference date answers HTTP 500 for a full read on our side. Build retries, log gaps explicitly, and don't backfill them with neighbouring months.</p>
<h2>The Timing Problem</h2>
<p>The feed is monthly. The deadline for existing providers to file phase 1 of their application is <strong>30 October 2026</strong> (Resolution 520, art. 88; Normative Instruction 704, art. 9). The restriction on regulated institutions starts <strong>6 November 2026</strong>. The first reference date that can reflect filings made up to the deadline is <strong>31 October 2026</strong>, and it will only be visible once the BCB publishes it.</p>
<p>So for the first stretch of November, a <code>not_found</code> against the September snapshot tells you very little: the firm may have filed in October. Your check should surface <code>as_of</code> prominently, and your policy config should say what happens when the newest data predates the deadline. Firms that filed on time may keep operating while their process runs (art. 88, Â§6). Firms that didn't must stop within 30 days of the deadline (art. 88, Â§7).</p>
<h2>Where to Go From Here</h2>
<ul>
<li>Run the check daily, but <strong>persist only when <code>DataBase</code> changes</strong>. Hash each snapshot and diff the process lists month to month. A process disappearing from the "open" list is a signal to look up its outcome, not a verdict.</li>
<li>For authorised status itself, take it from the BCB's authorisation data once SPSAVs start appearing there. As of the 30 September reference date, VASP Tracker found no SPSAV listed as authorised.</li>
<li>If you'd rather start from a curated mapping, our <a href="https://vasptracker.com/en/br/data/">Brazil open data</a> publishes 170 tracked platforms with their legal entities, CNPJ roots, SPSAV process numbers and BCB status per reference date (CSV/JSON, CC BY 4.0). Use it as a starting point, and verify against the BCB before acting.</li>
</ul>
<p><em>VASP Tracker is an independent project, not affiliated with Banco Central do Brasil or any regulator. This is an engineering write-up, not legal advice. How Article 91 applies to your institution is a question for your counsel and the BCB.</em></p>
<hr />
<h3>Sources</h3>
<ul>
<li>Banco Central do Brasil, ResoluÃ§Ã£o BCB nÂº 520 (10 Nov 2025), arts. 88 and 91: <a href="https://www.bcb.gov.br/estabilidadefinanceira/exibenormativo?tipo=Resolu%C3%A7%C3%A3o%20BCB&amp;numero=520">https://www.bcb.gov.br/estabilidadefinanceira/exibenormativo?tipo=Resolu%C3%A7%C3%A3o%20BCB&amp;numero=520</a></li>
<li>Banco Central do Brasil, ResoluÃ§Ã£o BCB nÂº 589 (23 Sep 2026), amending art. 91: <a href="https://www.bcb.gov.br/estabilidadefinanceira/exibenormativo?tipo=Resolu%C3%A7%C3%A3o%20BCB&amp;numero=589">https://www.bcb.gov.br/estabilidadefinanceira/exibenormativo?tipo=Resolu%C3%A7%C3%A3o%20BCB&amp;numero=589</a></li>
<li>Banco Central do Brasil, InstruÃ§Ã£o Normativa BCB nÂº 704, art. 9: <a href="https://www.bcb.gov.br/estabilidadefinanceira/exibenormativo?tipo=Instru%C3%A7%C3%A3o%20Normativa%20BCB&amp;numero=704">https://www.bcb.gov.br/estabilidadefinanceira/exibenormativo?tipo=Instru%C3%A7%C3%A3o%20Normativa%20BCB&amp;numero=704</a></li>
<li>BCB open data portal, "Processos de autorizaÃ§Ã£o" (ODbL): <a href="https://dadosabertos.bcb.gov.br/dataset/processos-de-autorizacao">https://dadosabertos.bcb.gov.br/dataset/processos-de-autorizacao</a></li>
<li>BCB Olinda API documentation, Processos_Autorizacao v1: <a href="https://olinda.bcb.gov.br/olinda/servico/Processos_Autorizacao/versao/v1/documentacao">https://olinda.bcb.gov.br/olinda/servico/Processos_Autorizacao/versao/v1/documentacao</a></li>
<li>VASP Tracker, Brazil open data (data dictionary and downloads): <a href="https://vasptracker.com/en/br/data/">https://vasptracker.com/en/br/data/</a></li>
</ul>
]]></content:encoded></item></channel></rss>