<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://ttylab80387.com/en/lending/</id>
    <title>ttylab80387 · Bitfinex Lending</title>
    <updated>2026-10-21T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://ttylab80387.com/en/lending/"/>
    <subtitle>The Bitfinex auto-lending bot I vibe-coded</subtitle>
    <icon>https://ttylab80387.com/en/img/favicon.svg</icon>
    <entry>
        <title type="html"><![CDATA[Bitfinex Lending Bot Dev Notes, Part 1: I Didn't Want to Pay for a Subscription, So I Wrote My Own]]></title>
        <id>https://ttylab80387.com/en/lending/bot-dev-01-rewrite/</id>
        <link href="https://ttylab80387.com/en/lending/bot-dev-01-rewrite/"/>
        <updated>2026-10-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Why I wrote my own lending bot, running other people's projects before rewriting it, and the problems of the first month: a 788,221% annual rate, chasing someone else's offer, UST that wasn't that UST, and the $150 minimum.]]></summary>
        <content type="html"><![CDATA[
<p>The first commit of this Bitfinex lending bot was on September 15, 2025, and it added 9,533 lines in one go. It's been about a year and 1,400-plus commits since then, which is a while, so this series is pieced back together from the git history, picking the places where the approach actually changed. This post covers how it started and the Bitfinex pitfalls of the first month.</p>
<!-- -->
<p>It started because I'd been using Cursor on a project at work, and the Claude model in it was pretty good, so I wondered whether I could use AI to write something fun.</p>
<p>Why a lending bot? I first came across Bitfinex lending in 2024, when I scrolled past a YouTuber promoting it. He made it sound like easy money, then suddenly switched to pitching a lending bot he'd written himself. I didn't feel like trying it back then, since I didn't want to pay for that bot's subscription. But now that I had some vibe coding experience, writing one for fun, and treating it as another investment, seemed like a good idea. Around August 2025 I looked at the MacBook Air M4 and decided it was good value for money, even though I really, really hate the notch. In the end I bought one just for writing code (computer prices have gone up like crazy since then, so now I actually regret not buying a model with better specs).</p>
<h2 class="anchor anchorTargetStickyNavbar_Emow" id="before-starting-running-other-peoples-projects">Before starting: running other people's projects<a href="https://ttylab80387.com/en/lending/bot-dev-01-rewrite/#before-starting-running-other-peoples-projects" class="hash-link" aria-label="Direct link to Before starting: running other people's projects" title="Direct link to Before starting: running other people's projects" translate="no">​</a></h2>
<p>I didn't start writing right away, because just getting through Bitfinex's KYC took a month.</p>
<p>Once lending was finally enabled on my account, the first step was to find a few lending bot projects on GitHub, download them and run them, changing things as I went.</p>
<p>Most of the projects I looked at were 7 to 8 years old, and I couldn't be bothered to set up the environments myself anyway, so I had AI read through them, and it set everything up and got them running.</p>
<p>Once I had a rough idea of how lending worked, I redesigned the architecture and UI and rewrote everything. The rewrite was done with Claude Code; the very first commit already lists Claude as co-author (Claude Code today is clearly far stronger than it was last year, and the subscription gives you a lot more usage too).</p>
<p>The reasons for choosing Go, as recorded in the development docs at the time:</p>
<ul>
<li class="">A single executable, so deployment is simple</li>
<li class="">goroutines can handle several currencies at the same time</li>
<li class="">Compiled and strongly typed, so fewer errors that only blow up at runtime</li>
<li class="">Low memory use; the doc said 70% less than Node.js (I never measured that number; AI probably wrote it on its own)</li>
<li class="">There's an official Go client to refer to (it later turned out it hasn't had new features since 2021, so I didn't end up using it)</li>
</ul>
<p>I also had a bit of a soft spot for Go: I'd often heard that Go performs well, and that a lot of gambling-industry systems are written in it. In practice, the performance turned out not to matter at all: today the bot runs 6 schedulers plus the web UI on a Raspberry Pi using about 50MB of memory and 0.08% CPU. Where Go really paid off was deployment: on the Mac it takes 15 seconds to build an executable the Raspberry Pi can run.</p>
<p>The first version was very ambitious, with multi-user support, an execution pool and a circuit breaker, and the next day those three pieces were deleted, 1,231 lines in all (I was the only user at the time). Back then the bot ran on the laptop; on September 23 I even added offline fault tolerance to handle losing the network connection when taking the laptop out. Docker and the move to a Raspberry Pi came the following January.</p>
<h2 class="anchor anchorTargetStickyNavbar_Emow" id="a-788221-annual-rate">A 788,221% annual rate<a href="https://ttylab80387.com/en/lending/bot-dev-01-rewrite/#a-788221-annual-rate" class="hash-link" aria-label="Direct link to A 788,221% annual rate" title="Direct link to A 788,221% annual rate" translate="no">​</a></h2>
<p>On September 26, every offer I submitted was rejected by Bitfinex:</p>
<div class="language-text codeBlockContainer_Ot__ theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_z6Hy"><pre tabindex="0" class="prism-code language-text codeBlock_Y5th thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_bbfM"><div class="token-line" style="color:#9CDCFE"><span class="token plain">Invalid rate: Please put an offer lower than 7% a day</span><br></div></code></pre></div></div>
<p>The annual rate worked out in that day's commit was 788,221%.</p>
<p>The cause was mixing two rate units. The Bitfinex API takes a daily rate as a decimal, for example 0.000238, meaning 0.0238% a day; the website and the app show an annual percentage, for example 8.715%. Get the conversion wrong once and you're off by a factor of 36,500 (365 days × 100).</p>
<p>How the fixes went:</p>
<ol>
<li class="">9/26: fixed the conversion in two files, the pricing one and the strategy one (worked)</li>
<li class="">10/2: rejected again, this time at 228,365%. Another function, the one that generates long-period high-rate offers, put the annual figure 10.0 straight in as a daily rate, and that day nothing got lent at all (didn't work: last time I had only fixed the two places that broke)</li>
<li class="">10/3: every place switched to the same pair of conversion functions, <code>DailyDecimalToAnnualPercent</code> and <code>AnnualPercentToDailyDecimal</code> (worked)</li>
<li class="">10/4: added a <code>RateValidator</code> that treats any daily rate above 0.2 as a format error (didn't work: from the day it was written until today, nothing has ever called it)</li>
</ol>
<p>Luckily, the conversion error made the rate too high, so the 7%-a-day cap stopped it; the cost was money sitting idle instead of being lent out.</p>
<h2 class="anchor anchorTargetStickyNavbar_Emow" id="the-11-urgent-order-i-chased-was-someone-elses-offer">The 11% urgent order I chased was someone else's offer<a href="https://ttylab80387.com/en/lending/bot-dev-01-rewrite/#the-11-urgent-order-i-chased-was-someone-elses-offer" class="hash-link" aria-label="Direct link to The 11% urgent order I chased was someone else's offer" title="Direct link to The 11% urgent order I chased was someone else's offer" translate="no">​</a></h2>
<p>On October 3 I added a feature to jump on high-rate orders: whenever the funding book showed an order at 9.5% or more for 30 days or longer, the bot would immediately place its own offer. The test log says it detected 11% for 120 days and really did place the offer.</p>
<p>In the early hours of the next day I found it had been looking at the wrong side. The Bitfinex funding book API splits the two sides by sign:</p>
<ul>
<li class="">Negative is a BID: the rate a borrower is willing to pay</li>
<li class="">Positive is an OFFER: the rate another lender is asking</li>
</ul>
<p>The code looked at the positive ones, so that 11% was a price another lender had put up, not someone wanting to borrow at 11%. After switching to the negative ones, the best BID at the time was only 7.1%, nowhere near the threshold. I don't remember whether that 11% offer ever filled.</p>
<p>Then I overcorrected. On 10/4 everything was changed to look only at BIDs, but deciding what rate to ask and where you'll rank in the queue actually depends on the competitors on the OFFER side. When that old strategy was deleted in November, the commit gave exactly this reason: "Analyzed BID orders instead of ASK". Look at the BID side to find opportunities and the OFFER side to set your price; you need both sides.</p>
<h2 class="anchor anchorTargetStickyNavbar_Emow" id="ust-isnt-that-ust">UST isn't that UST<a href="https://ttylab80387.com/en/lending/bot-dev-01-rewrite/#ust-isnt-that-ust" class="hash-link" aria-label="Direct link to UST isn't that UST" title="Direct link to UST isn't that UST" translate="no">​</a></h2>
<p>On the evening of October 9 I saw the front end querying USD, UST and USDT, assumed UST was TerraUSD, which collapsed in 2022, and removed it. Seven and a half hours later I found out that Bitfinex's wallet API uses UST for USDT (Tether): querying USDT on the front end returned nothing, so the USDT balance never showed up.</p>
<h2 class="anchor anchorTargetStickyNavbar_Emow" id="the-150-minimum">The $150 minimum<a href="https://ttylab80387.com/en/lending/bot-dev-01-rewrite/#the-150-minimum" class="hash-link" aria-label="Direct link to The $150 minimum" title="Direct link to The $150 minimum" translate="no">​</a></h2>
<p>On Bitfinex, a single USD lending offer has to be at least 150 dollars. Setting aside reserves, splitting offers and using pyramid allocation all left remainders under 150, and since those can't be placed, they sit idle; one commit recorded 39.8% idle. From late September to early October I gradually merged the remainders into the main offers, and idle money eventually dropped to 4.2%.</p>
<p>On October 9 I tried lowering the minimum to 50, and the API replied right away:</p>
<div class="language-text codeBlockContainer_Ot__ theme-code-block" style="--prism-color:#9CDCFE;--prism-background-color:#1E1E1E"><div class="codeBlockContent_z6Hy"><pre tabindex="0" class="prism-code language-text codeBlock_Y5th thin-scrollbar" style="color:#9CDCFE;background-color:#1E1E1E"><code class="codeBlockLines_bbfM"><div class="token-line" style="color:#9CDCFE"><span class="token plain">minimum is 150.0 dollar or equivalent in USD</span><br></div></code></pre></div></div>
<p>I changed it back a minute later. It wasn't the first time, either: the <code>BUSINESS_RULES.md</code> written the next day notes that Claude had tried several times to change min_amount to 50.0 to raise fund utilization, so I simply made 150 an iron rule that must never be changed (AI sees idle money and wants to touch the minimum). That rule is still in the project's <code>CLAUDE.md</code> (the project notes written for the AI) today.</p>
<h2 class="anchor anchorTargetStickyNavbar_Emow" id="the-log-said-follow_trade-but-something-else-was-running">The log said follow_trade, but something else was running<a href="https://ttylab80387.com/en/lending/bot-dev-01-rewrite/#the-log-said-follow_trade-but-something-else-was-running" class="hash-link" aria-label="Direct link to The log said follow_trade, but something else was running" title="Direct link to The log said follow_trade, but something else was running" translate="no">​</a></h2>
<p>The September 20 refactor left a <code>FORCED OVERRIDE</code> in the scheduler that ran IntelligentStrategy no matter what the settings said. On September 28 I added follow_trade (placing offers at the market's actual traded rates) and the settings were switched to it, but the override was still there, so the logs recorded the strategy as follow_trade while Intelligent was what actually ran. follow_trade's trade analysis also used hard-coded fake data and always returned 6.99%. I didn't notice at the time; I only found the mismatch later, and both problems were removed together on October 3.</p>
<p>Looking back now, a lot of the bugs came from not knowing the business rules of lending well, plus some ambiguous signs and symbols. I remember checking them properly at the time, but the AI would get too clever, decide they meant something else, and the bugs kept coming. Still, finally getting the bot running was a lot of fun.</p>]]></content>
        <category label="Dev Notes" term="Dev Notes"/>
    </entry>
</feed>