<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: FoxyyyBusiness</title>
    <description>The latest articles on Forem by FoxyyyBusiness (@foxyyybusiness).</description>
    <link>https://forem.com/foxyyybusiness</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3870571%2F4e0f35eb-daa9-41df-86fd-f5432fe83410.png</url>
      <title>Forem: FoxyyyBusiness</title>
      <link>https://forem.com/foxyyybusiness</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/foxyyybusiness"/>
    <language>en</language>
    <item>
      <title>I shipped 100+ free tools on a $5 VPS in 5 days</title>
      <dc:creator>FoxyyyBusiness</dc:creator>
      <pubDate>Sat, 11 Apr 2026 22:23:15 +0000</pubDate>
      <link>https://forem.com/foxyyybusiness/i-shipped-100-free-tools-on-a-5-vps-in-5-days-18pn</link>
      <guid>https://forem.com/foxyyybusiness/i-shipped-100-free-tools-on-a-5-vps-in-5-days-18pn</guid>
      <description>&lt;p&gt;I've been building foxyyy.com — a collection of 100+ free developer and utility tools running on a single $5 VPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server&lt;/strong&gt;: $5 VPS (1 vCPU, 1GB RAM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Flask + nginx reverse proxy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Pure HTML/CSS/JS — no frameworks, no CDN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design&lt;/strong&gt;: Dark theme, monospace fonts, amber accent (#c9a227)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;i18n&lt;/strong&gt;: Client-side EN/FR/ES/DE language selector on every page&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's included (110 tools)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Developer Tools
&lt;/h3&gt;

&lt;p&gt;JSON Formatter, Regex Tester, Base64/JWT Decoder, Hash Generator (MD5/SHA), URL Encoder, HTML Entities, Diff Checker, CSS Minifier, UUID Generator, Timestamp Converter, Markdown Preview, HTTP Status Codes, DNS Lookup, WHOIS Lookup, Cron Explainer, Sitemap Generator, Meta Tag Generator, Robots.txt Generator, Chmod Calculator, IP Subnet Calculator, CSV to JSON, JSON to TypeScript, SQL Formatter, Markdown Table Generator, Binary/Hex/Decimal Converter&lt;/p&gt;

&lt;h3&gt;
  
  
  Business &amp;amp; Finance (FR)
&lt;/h3&gt;

&lt;p&gt;TVA Calculator, Salaire Net/Brut, Auto-Entrepreneur Simulator, Prêt Immobilier, Mentions Légales Generator, Privacy Policy Generator, Invoice PDF Generator, Stripe Fee Calculator&lt;/p&gt;

&lt;h3&gt;
  
  
  Content &amp;amp; Media
&lt;/h3&gt;

&lt;p&gt;Color Picker, Palette Generator, Gradient Generator, Placeholder Images, OG Preview, Favicon Generator, Image Resizer, Emoji Picker, Lorem Ipsum (FR/EN), ASCII Art, Typing Speed Test, Word Counter&lt;/p&gt;

&lt;h3&gt;
  
  
  Utilities
&lt;/h3&gt;

&lt;p&gt;Password Generator, QR Code Generator, Email Deliverability Checker, Pomodoro Timer, Stopwatch, Countdown Timer, IP Lookup, Random Number Generator, Unit Converter, BMI Calculator, Age Calculator, Percentage Calculator, Aspect Ratio Calculator&lt;/p&gt;

&lt;h3&gt;
  
  
  Combo Dashboards
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dev Toolkit&lt;/strong&gt;: 6 dev tools in one tabbed interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO Audit&lt;/strong&gt;: Meta tags + DNS + OG + speed check&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain Health Check&lt;/strong&gt;: DNS + WHOIS + Email + SSL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color Design Suite&lt;/strong&gt;: Picker + Palette + Gradient + Contrast&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entrepreneur Dashboard&lt;/strong&gt;: French AE business calculator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text Suite&lt;/strong&gt;: Word counter + case converter + diff + lorem + find/replace&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Design philosophy
&lt;/h2&gt;

&lt;p&gt;Every tool is a single HTML file. No build step. No npm. The whole thing loads in under 8KB per page. Dark/light theme toggle on every page. Zero tracking, zero ads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;110 tools shipped in 5 days&lt;/li&gt;
&lt;li&gt;87 HTML files&lt;/li&gt;
&lt;li&gt;~600KB total static assets&lt;/li&gt;
&lt;li&gt;1 VPS, 1 developer&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;More combo dashboards&lt;/li&gt;
&lt;li&gt;API endpoints for automation&lt;/li&gt;
&lt;li&gt;More languages&lt;/li&gt;
&lt;li&gt;Community contributions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it: &lt;a href="https://foxyyy.com" rel="noopener noreferrer"&gt;foxyyy.com&lt;/a&gt;&lt;br&gt;
Changelog: &lt;a href="https://foxyyy.com/changelog" rel="noopener noreferrer"&gt;foxyyy.com/changelog&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built solo. No funding. Just shipping.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>6 outils gratuits que j'ai construits pour les auto-entrepreneurs français</title>
      <dc:creator>FoxyyyBusiness</dc:creator>
      <pubDate>Fri, 10 Apr 2026 22:46:43 +0000</pubDate>
      <link>https://forem.com/foxyyybusiness/6-outils-gratuits-que-jai-construits-pour-les-auto-entrepreneurs-francais-5a0b</link>
      <guid>https://forem.com/foxyyybusiness/6-outils-gratuits-que-jai-construits-pour-les-auto-entrepreneurs-francais-5a0b</guid>
      <description>&lt;p&gt;Je suis développeur et j'ai créé une suite d'outils en ligne gratuits pour les besoins courants des auto-entrepreneurs et freelances en France. Tout est gratuit, sans inscription, sans pub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les outils
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Simulateur de charges auto-entrepreneur
&lt;/h3&gt;

&lt;p&gt;Entrez votre CA mensuel, l'outil calcule :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Charges URSSAF (taux selon votre activité : BNC 21.1%, BIC 21.2%, commerce 12.3%)&lt;/li&gt;
&lt;li&gt;Impôt sur le revenu (versement libératoire ou barème progressif)&lt;/li&gt;
&lt;li&gt;Net réel après tous les prélèvements&lt;/li&gt;
&lt;li&gt;Vue mensuelle et annuelle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://foxyyy.com/simulateur" rel="noopener noreferrer"&gt;foxyyy.com/simulateur&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Calculateur TVA
&lt;/h3&gt;

&lt;p&gt;Conversion instantanée HT ↔ TTC avec les 4 taux français :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20% (normal)&lt;/li&gt;
&lt;li&gt;10% (intermédiaire : restauration, transport)&lt;/li&gt;
&lt;li&gt;5,5% (réduit : alimentation, énergie)&lt;/li&gt;
&lt;li&gt;2,1% (super-réduit : presse, médicaments)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://foxyyy.com/tva/" rel="noopener noreferrer"&gt;foxyyy.com/tva&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Calculateur salaire net/brut
&lt;/h3&gt;

&lt;p&gt;Convertisseur brut ↔ net pour :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cadres&lt;/li&gt;
&lt;li&gt;Non-cadres&lt;/li&gt;
&lt;li&gt;Fonction publique&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avec le détail des prélèvements : sécurité sociale, CSG/CRDS, retraite complémentaire, chômage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://foxyyy.com/salaire" rel="noopener noreferrer"&gt;foxyyy.com/salaire&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Générateur de factures PDF
&lt;/h3&gt;

&lt;p&gt;Formulaire → facture PDF conforme :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mentions obligatoires auto-entrepreneur (article 293B du CGI)&lt;/li&gt;
&lt;li&gt;TVA optionnelle&lt;/li&gt;
&lt;li&gt;Téléchargement instantané&lt;/li&gt;
&lt;li&gt;Aussi disponible en API : &lt;code&gt;POST /api/generate&lt;/code&gt; avec un JSON → PDF&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://foxyyy.com/invoice/" rel="noopener noreferrer"&gt;foxyyy.com/invoice&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Générateur de mentions légales
&lt;/h3&gt;

&lt;p&gt;Obligatoire pour tout site web en France. L'outil génère des mentions conformes selon votre statut :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-entrepreneur, SARL, SAS, EURL, association&lt;/li&gt;
&lt;li&gt;Section RGPD/cookies si applicable&lt;/li&gt;
&lt;li&gt;Hébergeur pré-rempli (OVH par défaut)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://foxyyy.com/legal/" rel="noopener noreferrer"&gt;foxyyy.com/legal&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Politique de confidentialité RGPD
&lt;/h3&gt;

&lt;p&gt;Générateur détaillé avec :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Données collectées (email, nom, IP, cookies...)&lt;/li&gt;
&lt;li&gt;Finalités du traitement&lt;/li&gt;
&lt;li&gt;Base légale (consentement, contrat, intérêt légitime)&lt;/li&gt;
&lt;li&gt;Droits des utilisateurs (accès, rectification, effacement...)&lt;/li&gt;
&lt;li&gt;Sous-traitants (Google Analytics, Stripe, Mailchimp...)&lt;/li&gt;
&lt;li&gt;Durée de conservation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://foxyyy.com/privacy" rel="noopener noreferrer"&gt;foxyyy.com/privacy&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Pourquoi c'est gratuit ?
&lt;/h2&gt;

&lt;p&gt;Ces outils tournent sur un VPS à 5€/mois. Le coût marginal d'un utilisateur supplémentaire est quasi nul. J'ai d'autres produits payants sur la même plateforme (pour les développeurs), mais ces outils business sont et resteront gratuits.&lt;/p&gt;

&lt;h2&gt;
  
  
  La page hub
&lt;/h2&gt;

&lt;p&gt;Tous les outils FR sont regroupés ici : &lt;strong&gt;&lt;a href="https://foxyyy.com/outils-fr" rel="noopener noreferrer"&gt;foxyyy.com/outils-fr&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si vous avez des suggestions d'outils qui manquent pour les indépendants en France, je suis preneur dans les commentaires.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tous les outils sur &lt;a href="https://foxyyy.com" rel="noopener noreferrer"&gt;foxyyy.com&lt;/a&gt;. Code par &lt;a href="https://x.com/foxyyybusiness" rel="noopener noreferrer"&gt;Clément Slowik&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>french</category>
      <category>entrepreneur</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I shipped 41 tools on a $5 VPS in 4 days — here is everything I learned</title>
      <dc:creator>FoxyyyBusiness</dc:creator>
      <pubDate>Fri, 10 Apr 2026 19:22:23 +0000</pubDate>
      <link>https://forem.com/foxyyybusiness/i-shipped-41-tools-on-a-5-vps-in-4-days-here-is-everything-i-learned-47oj</link>
      <guid>https://forem.com/foxyyybusiness/i-shipped-41-tools-on-a-5-vps-in-4-days-here-is-everything-i-learned-47oj</guid>
      <description>&lt;p&gt;Four days ago I started a 30-day challenge: ship as many useful tools as possible on a single $5 VPS, using Python + Flask + SQLite + systemd. No Docker, no Kubernetes, no cloud functions.&lt;/p&gt;

&lt;p&gt;Today the counter is at &lt;strong&gt;41 tools across 9 categories&lt;/strong&gt;, all live at &lt;a href="https://foxyyy.com" rel="noopener noreferrer"&gt;foxyyy.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I shipped
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Crypto &amp;amp; Trading&lt;/strong&gt; (the starting point):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-exchange funding rate scanner (20 exchanges, ~6,800 perps)&lt;/li&gt;
&lt;li&gt;Autonomous signal alerts bot&lt;/li&gt;
&lt;li&gt;Exchange uptime tracker&lt;/li&gt;
&lt;li&gt;Historical dataset (2.58M rows, CC BY 4.0)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Developer Tools&lt;/strong&gt; (the largest category):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/regex" rel="noopener noreferrer"&gt;Regex Tester&lt;/a&gt; — live matching with capture groups&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/json" rel="noopener noreferrer"&gt;JSON Formatter&lt;/a&gt; — validate + pretty-print + minify&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/cron/" rel="noopener noreferrer"&gt;Cron Explainer&lt;/a&gt; — paste a cron expression, get English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/decode" rel="noopener noreferrer"&gt;Base64/JWT Decoder&lt;/a&gt; — client-side, nothing sent to server&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/uuid" rel="noopener noreferrer"&gt;UUID Generator&lt;/a&gt;, &lt;a href="https://foxyyy.com/timestamp" rel="noopener noreferrer"&gt;Timestamp Converter&lt;/a&gt;, &lt;a href="https://foxyyy.com/http-codes" rel="noopener noreferrer"&gt;HTTP Status Codes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/md2pdf/" rel="noopener noreferrer"&gt;Markdown to PDF&lt;/a&gt; — paste markdown, download styled A4 PDF&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/screenshot/" rel="noopener noreferrer"&gt;Screenshot API&lt;/a&gt; — capture any webpage as PNG&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/clementslowik/cronviz" rel="noopener noreferrer"&gt;cronviz&lt;/a&gt; — OSS CLI for cron/systemd observability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;French Business Tools&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/invoice/" rel="noopener noreferrer"&gt;Invoice PDF Generator&lt;/a&gt; — auto-entrepreneur compliant&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/legal/" rel="noopener noreferrer"&gt;Mentions Légales&lt;/a&gt; — French legal notice generator&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/tva/" rel="noopener noreferrer"&gt;TVA Calculator&lt;/a&gt; — all 4 French VAT rates&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/salaire" rel="noopener noreferrer"&gt;Salary Net/Brut&lt;/a&gt; — cadre/non-cadre converter&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/simulateur" rel="noopener noreferrer"&gt;Auto-Entrepreneur Simulator&lt;/a&gt; — URSSAF + tax → net&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Utilities&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/email-check/" rel="noopener noreferrer"&gt;Email Checker&lt;/a&gt; — SPF/DKIM/DMARC analysis with score&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/dns" rel="noopener noreferrer"&gt;DNS Lookup&lt;/a&gt;, &lt;a href="https://foxyyy.com/whois" rel="noopener noreferrer"&gt;WHOIS&lt;/a&gt;, &lt;a href="https://foxyyy.com/my-ip" rel="noopener noreferrer"&gt;IP Lookup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/password/" rel="noopener noreferrer"&gt;Password Generator&lt;/a&gt; with entropy calculation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/qr/" rel="noopener noreferrer"&gt;QR Code Generator&lt;/a&gt; — PNG + SVG&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/uptime/" rel="noopener noreferrer"&gt;Uptime Monitor&lt;/a&gt; — 5-min pings&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/colors/" rel="noopener noreferrer"&gt;Color Palette&lt;/a&gt;, &lt;a href="https://foxyyy.com/favicon-gen" rel="noopener noreferrer"&gt;Favicon Generator&lt;/a&gt;, &lt;a href="https://foxyyy.com/lorem" rel="noopener noreferrer"&gt;Lorem Ipsum FR&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://foxyyy.com/words" rel="noopener noreferrer"&gt;Word Counter&lt;/a&gt;, &lt;a href="https://foxyyy.com/case" rel="noopener noreferrer"&gt;Text Case Converter&lt;/a&gt;, &lt;a href="https://foxyyy.com/focus" rel="noopener noreferrer"&gt;Pomodoro Timer&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And more. Full changelog: &lt;a href="https://foxyyy.com/changelog" rel="noopener noreferrer"&gt;foxyyy.com/changelog&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Every tool runs on &lt;strong&gt;one $5 OVH VPS&lt;/strong&gt; (2 vCPU, 2GB RAM):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.12 + Flask&lt;/li&gt;
&lt;li&gt;SQLite (WAL mode) for anything that needs persistence&lt;/li&gt;
&lt;li&gt;systemd for every service (17 active units)&lt;/li&gt;
&lt;li&gt;nginx as reverse proxy + Let's Encrypt HTTPS&lt;/li&gt;
&lt;li&gt;No Docker, no Redis, no Postgres&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total memory usage across all 17 services: ~400 MB. CPU mostly idle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client-side tools are free to ship.&lt;/strong&gt; Half of these tools (regex, base64, JSON, UUID, pomodoro, etc.) are pure JavaScript. No server process, no port, no systemd unit. Just an HTML file with &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags. Once routed, the cost of hosting them is literally zero.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The boring stack scales to dozens of services.&lt;/strong&gt; systemd + SQLite + Flask is enough for everything I've built. Each service starts in &amp;lt;1 second, uses ~20-40MB, and auto-restarts on failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Distribution is harder than building.&lt;/strong&gt; I can ship a tool in 30 minutes. Getting 10 people to see it takes 10x longer. My Twitter account has 2 followers. My dev.to profile is brand new. The tools are invisible without external traffic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A pricing page makes the free tools feel more valuable.&lt;/strong&gt; Once I added a &lt;a href="https://foxyyy.com/plans" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; that shows "30+ tools are free, 4 are paid", the free tools stopped feeling like side projects and started feeling like a product.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Monetization
&lt;/h2&gt;

&lt;p&gt;4 products have Stripe payment links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;30 Boring Patterns&lt;/strong&gt; — €19 one-time (production recipes for solo devs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flask SaaS Starter Kit&lt;/strong&gt; — €29 one-time (boilerplate with auth + billing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshot API Pro&lt;/strong&gt; — €9/month (higher rate limit)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email Checker Pro&lt;/strong&gt; — €5/month (bulk API)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Revenue so far: €0. Day 4. The funnel exists, the traffic doesn't yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;26 more days. More tools in more domains. The goal is to find which tool has organic traction and double down on it. Every tool is a lottery ticket — the more I ship, the higher the chance one breaks through.&lt;/p&gt;

&lt;p&gt;If you want to explore: &lt;a href="https://foxyyy.com" rel="noopener noreferrer"&gt;foxyyy.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://x.com/foxyyybusiness" rel="noopener noreferrer"&gt;Clément Slowik&lt;/a&gt;. All tools open on foxyyy.com. OSS repos on &lt;a href="https://github.com/clementslowik" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>python</category>
    </item>
    <item>
      <title>The funding-rate gotcha that breaks 30% of Python crypto scrapers: per-symbol intervals</title>
      <dc:creator>FoxyyyBusiness</dc:creator>
      <pubDate>Fri, 10 Apr 2026 11:27:05 +0000</pubDate>
      <link>https://forem.com/foxyyybusiness/the-funding-rate-gotcha-that-breaks-30-of-python-crypto-scrapers-per-symbol-intervals-54cj</link>
      <guid>https://forem.com/foxyyybusiness/the-funding-rate-gotcha-that-breaks-30-of-python-crypto-scrapers-per-symbol-intervals-54cj</guid>
      <description>&lt;p&gt;If you've ever used or written a Python script to scrape crypto perpetual funding rates from Binance, Bybit, Bitget, MEXC, Gate.io, OKX, or Hyperliquid, there's a 70% chance you have a subtle bug that quietly inflates your annualized yields by 2-8x on roughly 30% of symbols.&lt;/p&gt;

&lt;p&gt;I'm not exaggerating. I checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;Every popular GitHub repo I've found that scrapes funding rates assumes an &lt;strong&gt;8-hour funding interval&lt;/strong&gt; for every symbol. The annualization is typically computed as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;annualized_yield_pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;funding_rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;3&lt;/code&gt; is "3 funding settlements per day" because there are 24 hours in a day and 8 hours per funding period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is wrong for ~30% of perpetual symbols on Binance and Bybit, ~25% of Bitget, and 100% of Hyperliquid and dYdX.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Binance has 425 USDT-margined perps on a 4-hour cycle and 4 perps on a 1-hour cycle. Bybit has 377 on 4-hour and 3 on 1-hour. Bitget has a similar mix. Hyperliquid and dYdX v4 are entirely on 1-hour cycles. MEXC even has a few perps on 24-hour cycles.&lt;/p&gt;

&lt;p&gt;A 0.05% per-period funding rate is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;55% APY&lt;/strong&gt; if the symbol is on an 8h cycle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;109% APY&lt;/strong&gt; if it's 4h&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;438% APY&lt;/strong&gt; if it's 1h&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;18% APY&lt;/strong&gt; if it's 24h&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your scanner shows the same annualized number for all of them — or worse, applies 8h to all of them — you're either over-reporting or under-reporting by 2-8x on roughly a third of your universe. The mistake silently propagates into your "best opportunities" ranking and leads to wasted capital on misranked trades.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify whether your code has the bug
&lt;/h2&gt;

&lt;p&gt;Pick any USDT-margined perp from one of the affected exchanges that has a non-standard interval. A few examples that were on 4h funding intervals as of writing (always check the live values, exchanges shift their cycles):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binance: many small-cap altcoins, especially recently listed&lt;/li&gt;
&lt;li&gt;Bybit: similar pattern&lt;/li&gt;
&lt;li&gt;Hyperliquid: literally all of them are 1h&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run your scraper. Look at the symbol's annualized yield. Then manually compute the correct number using the actual funding interval. If they differ by 2x or more, you have the bug.&lt;/p&gt;

&lt;p&gt;Or just look for the magic number &lt;code&gt;3 * 365&lt;/code&gt; (or &lt;code&gt;1095&lt;/code&gt;) in your code. If it's there without a &lt;code&gt;funding_interval_hours&lt;/code&gt; lookup nearby, you have the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Each exchange exposes the funding interval per symbol in a different endpoint, with a different field name, and a different unit. Welcome to crypto data plumbing. Here's the per-exchange fix:&lt;/p&gt;

&lt;h3&gt;
  
  
  Binance
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_binance_funding_intervals&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;{symbol: hours}. Cache process-wide, refresh weekly.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://fapi.binance.com/fapi/v1/fundingInfo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fundingIntervalHours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint only returns symbols with non-default intervals (i.e. anything that's not 8h). For symbols not in the response, assume 8h. Cache this dict at startup and refresh once a week — the assignments change rarely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bybit
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_bybit_funding_intervals&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Bybit returns the interval in MINUTES via instruments-info.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;linear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.bybit.com/v5/market/instruments-info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
            &lt;span class="n"&gt;sym&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fundingInterval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# default 8h = 480 min
&lt;/span&gt;            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nextPageCursor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the unit: &lt;strong&gt;minutes&lt;/strong&gt;. Convert to hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bitget
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_bitget_funding_intervals&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Returned in HOURS as fundingRateInterval. Single bulk call.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.bitget.com/api/v2/mix/market/current-fund-rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;productType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usdt-futures&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fundingRateInterval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cleanest of the bunch. Bulk endpoint, hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  MEXC
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_mexc_funding_intervals_inline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;funding_rate_response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;MEXC bundles the interval into the funding_rate response itself, as `collectCycle`.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;collectCycle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;funding_rate_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inline with the funding rate data — no separate call needed. The field is &lt;code&gt;collectCycle&lt;/code&gt; and the unit is hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gate.io
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_gateio_funding_intervals&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Gate.io exposes funding_interval in SECONDS via /futures/usdt/contracts.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.gateio.ws/api/v4/futures/usdt/contracts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;funding_interval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28800&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# default 8h = 28800s
&lt;/span&gt;        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the unit: &lt;strong&gt;seconds&lt;/strong&gt;. Divide by 3600.&lt;/p&gt;

&lt;h3&gt;
  
  
  OKX
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_okx_funding_intervals_from_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;funding_rate_response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;OKX doesn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t expose the interval directly. Compute it from
    nextFundingTime - fundingTime in the funding-rate response.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;next_t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;funding_rate_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nextFundingTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;prev_t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;funding_rate_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fundingTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;next_t&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;prev_t&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;next_t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;prev_t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;next_t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prev_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OKX is the trickiest. They don't expose the interval as a field — you compute it from the time difference between consecutive settlements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hyperliquid and dYdX v4
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;HYPERLIQUID_INTERVAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;   &lt;span class="c1"&gt;# always 1h
&lt;/span&gt;&lt;span class="n"&gt;DYDX_INTERVAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;          &lt;span class="c1"&gt;# always 1h
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two venues have a single funding interval across their entire universe. No lookup needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The corrected annualization
&lt;/h2&gt;

&lt;p&gt;Once you have the per-symbol interval, the annualization is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;annualize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;funding_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interval_hours&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;interval_hours&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;periods_per_year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;interval_hours&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;funding_rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;periods_per_year&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;  &lt;span class="c1"&gt;# in %
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an 8h symbol, this gives &lt;code&gt;funding_rate * 1095 * 100&lt;/code&gt;.&lt;br&gt;
For a 4h symbol: &lt;code&gt;funding_rate * 2190 * 100&lt;/code&gt;.&lt;br&gt;
For a 1h symbol: &lt;code&gt;funding_rate * 8760 * 100&lt;/code&gt;.&lt;br&gt;
For a 24h symbol: &lt;code&gt;funding_rate * 365 * 100&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's it. Five lines of code, applied per symbol with the correct interval, and your scanner is no longer lying to you.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I see in the wild
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://foxyyy.com/" rel="noopener noreferrer"&gt;Funding Finder&lt;/a&gt; partly because none of the public alternatives I tried got this right. The free n8n templates assume 8h. The popular GitHub repos assume 8h. Even some commercial scanners I tried showed obvious signs of this bug — claiming 200% APY on a 1h-funding Hyperliquid perp where the actual rate was modest.&lt;/p&gt;

&lt;p&gt;Coinglass gets it right (they're a serious commercial product). Coinalyze is BTC-only so it doesn't matter. Most everything else gets it wrong.&lt;/p&gt;

&lt;p&gt;If you maintain a public crypto data scraper or scanner, please fix this. The fix is small and the impact on user trust is significant.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sample broken code from a popular GitHub repo (anonymized)
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Found in a public repo with 800+ stars
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_funding_apy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1095&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;  &lt;span class="c1"&gt;# 3 settlements per day * 365 days
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Not naming the repo, but I've found this exact pattern in at least 5 popular crypto scrapers. If you wrote one of them, you know who you are. The fix is 20 lines.&lt;/p&gt;
&lt;h2&gt;
  
  
  The OSS data layer that gets it right
&lt;/h2&gt;

&lt;p&gt;I extracted the data plumbing from Funding Finder into an MIT-licensed Python package called &lt;a href="https://github.com/clementslowik/funding-collector" rel="noopener noreferrer"&gt;funding-collector&lt;/a&gt; that handles the per-symbol funding interval correctly across 8 exchanges (Binance, Bybit, OKX, Bitget, MEXC, Hyperliquid, Gate.io, dYdX v4). 700 lines of Python, no async, no dependencies beyond &lt;code&gt;requests&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;funding-collector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or just copy the relevant &lt;code&gt;_&amp;lt;exchange&amp;gt;_funding_intervals()&lt;/code&gt; function from this post into your existing scraper. The fix takes 20 minutes.&lt;/p&gt;

&lt;p&gt;— Clément&lt;/p&gt;

</description>
      <category>python</category>
      <category>crypto</category>
      <category>api</category>
      <category>bug</category>
    </item>
    <item>
      <title>Migrating from Coinglass to Funding Finder: a step-by-step walkthrough for funding-rate arbitrage traders</title>
      <dc:creator>FoxyyyBusiness</dc:creator>
      <pubDate>Fri, 10 Apr 2026 10:35:20 +0000</pubDate>
      <link>https://forem.com/foxyyybusiness/migrating-from-coinglass-to-funding-finder-a-step-by-step-walkthrough-for-funding-rate-arbitrage-324n</link>
      <guid>https://forem.com/foxyyybusiness/migrating-from-coinglass-to-funding-finder-a-step-by-step-walkthrough-for-funding-rate-arbitrage-324n</guid>
      <description>&lt;p&gt;This post is for the specific subset of Coinglass users who use the platform mostly for &lt;strong&gt;cross-exchange funding-rate arbitrage data&lt;/strong&gt; and have wondered if there's a cheaper, more focused alternative. There is. I built it. This is a 10-minute migration guide.&lt;/p&gt;

&lt;p&gt;If you use Coinglass for liquidations, ETF flows, on-chain data, options flow, or the broader derivatives surface, &lt;strong&gt;this post is not for you and you should stay on Coinglass&lt;/strong&gt;. It's a great product for that breadth.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before (Coinglass Hobbyist)&lt;/th&gt;
&lt;th&gt;After (Funding Finder Trader)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;$29/month&lt;/td&gt;
&lt;td&gt;€5/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit&lt;/td&gt;
&lt;td&gt;30 req/min&lt;/td&gt;
&lt;td&gt;600 req/min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exchanges&lt;/td&gt;
&lt;td&gt;~30&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Funding-rate history&lt;/td&gt;
&lt;td&gt;180 days&lt;/td&gt;
&lt;td&gt;30 days (free tier 24h)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API key required&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Optional (free tier no key needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-exchange arb endpoint&lt;/td&gt;
&lt;td&gt;No native&lt;/td&gt;
&lt;td&gt;Yes, ranked + filtered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open source data layer&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (MIT)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The math: you save $24/month if you only use the funding-rate features. The trade-off: smaller exchange coverage, less history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Audit your current Coinglass usage
&lt;/h2&gt;

&lt;p&gt;Before you migrate, run through your actual usage of Coinglass over the last 30 days. Be honest. Most users overestimate what they actually use.&lt;/p&gt;

&lt;p&gt;Ask yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What endpoints am I calling?&lt;/strong&gt; If your API logs show 90%+ of calls hit the funding-rate endpoints, the migration math is straightforward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which exchanges do I actually need?&lt;/strong&gt; Coinglass covers ~30 venues but most users only consume data from the top 5-8.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How much historical data do I actually use?&lt;/strong&gt; If you mostly look at the last week, the 30-day history of the Trader tier is plenty. If you regularly query 90+ days back, you'll need the Pro tier or stay on Coinglass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I use any non-funding endpoint?&lt;/strong&gt; Liquidations, OI, ETF flows, etc. If yes, stay on Coinglass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run this audit honestly. About 60% of "Coinglass for funding-arb" users I've talked to discover they only use 5-10% of what they pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Try the Funding Finder free tier
&lt;/h2&gt;

&lt;p&gt;Funding Finder has a no-signup free tier that covers 95% of the data you'd get from a paid Coinglass plan, with one critical caveat: history is capped at 24 hours instead of 180 days.&lt;/p&gt;

&lt;p&gt;Try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Current funding rates across all 8 exchanges&lt;/span&gt;
curl &lt;span class="s1"&gt;'https://foxyyy.com/api/funding/current'&lt;/span&gt; | jq &lt;span class="s1"&gt;'.count, .rows[0]'&lt;/span&gt;

&lt;span class="c"&gt;# Cross-exchange arbitrage opportunities, sorted by annualized yield&lt;/span&gt;
curl &lt;span class="s1"&gt;'https://foxyyy.com/api/funding/arb?min_yield=10&amp;amp;min_volume=10000000&amp;amp;limit=10'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.opportunities[] | {base, long_exchange, short_exchange, annualized_yield_pct}'&lt;/span&gt;

&lt;span class="c"&gt;# History for BTC (24h on free tier)&lt;/span&gt;
curl &lt;span class="s1"&gt;'https://foxyyy.com/api/funding/history/BTC?hours=24'&lt;/span&gt; | jq &lt;span class="s1"&gt;'.count'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run those three commands. If the data shape and content match what you need, the migration is feasible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Map your Coinglass API calls to Funding Finder equivalents
&lt;/h2&gt;

&lt;p&gt;Here's the rough mapping for the funding-rate-specific endpoints:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Coinglass&lt;/th&gt;
&lt;th&gt;Funding Finder&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /api/futures/fundingRate/v2/list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /api/funding/current&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /api/futures/fundingRate/ohlc-history&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /api/funding/history/&amp;lt;base&amp;gt;?hours=N&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /api/futures/fundingRate/exchange-list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /api/funding/by_exchange/&amp;lt;ex&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /api/futures/fundingRate/arbitrage-list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /api/funding/arb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(no equivalent)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /api/funding/extremes?n=20&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(no equivalent)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET /api/summary&lt;/code&gt; (sentiment + top arb in one call)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The response shapes are different, of course, so you'll need to update your client code. The biggest difference: Funding Finder normalizes everything to a single dict shape regardless of which exchange the data came from, which is one less thing for you to handle.&lt;/p&gt;

&lt;p&gt;A typical Coinglass response loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before (Coinglass)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;coinglass&lt;/span&gt;  &lt;span class="c1"&gt;# hypothetical client
&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coinglass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fundingRate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;v2list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Binance&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fundingRate&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# already a percent? or decimal? check docs
&lt;/span&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bybit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fundingRate&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# different scale, normalize manually
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# After (Funding Finder)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://foxyyy.com/api/funding/current&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;funding_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;              &lt;span class="c1"&gt;# always decimal, always per-period
&lt;/span&gt;    &lt;span class="n"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;funding_rate_pct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="c1"&gt;# already converted to %
&lt;/span&gt;    &lt;span class="n"&gt;apy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;annualized_pct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;             &lt;span class="c1"&gt;# already correctly annualized per-symbol
&lt;/span&gt;    &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;funding_interval_hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# always present, always correct
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Funding Finder schema does the unit conversion and normalization for you, including the per-symbol funding interval (which most other tools get wrong).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Update your alerting
&lt;/h2&gt;

&lt;p&gt;If you use Coinglass alerts, you'll need to migrate them to Funding Finder alerts. The endpoints are documented at &lt;a href="https://foxyyy.com/docs#alerts" rel="noopener noreferrer"&gt;/docs#alerts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sample alert creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get a paid tier API key first (Trader €5/mo)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'https://foxyyy.com/api/alerts/create'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$YOUR_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "base": "BTC",
    "min_yield_pct": 100,
    "min_volume_usd": 5000000,
    "telegram_chat_id": "12345",
    "cooldown_seconds": 3600
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The alert worker scans every 60 seconds and sends you a Telegram message when an opportunity matches. Cooldown prevents spam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Run both in parallel for one week
&lt;/h2&gt;

&lt;p&gt;This is the part most people skip and then regret. Don't cancel Coinglass yet. Run both APIs in parallel for one week, log every funding-rate query you make to both, and compare the results. You're looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coverage gaps&lt;/strong&gt;: any symbol you queried on Coinglass that's not in Funding Finder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numerical discrepancies&lt;/strong&gt;: same symbol, different funding rate? Investigate which one is right (usually the issue is that Coinglass shows the predicted next rate while Funding Finder shows the latest published rate)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency differences&lt;/strong&gt;: how fresh is the data on each side&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After a week, you'll have a clear answer to "is this migration worth it for my workflow". If the answer is yes, cancel Coinglass and pocket the $24/month difference. If the answer is no, you've lost €5 and gained the data you needed to make the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Self-host the data layer (optional)
&lt;/h2&gt;

&lt;p&gt;If you really care about cost and you're a developer, you can skip the hosted Funding Finder entirely and run the open-source data collector on your own VPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/clementslowik/funding-collector
&lt;span class="nb"&gt;cd &lt;/span&gt;funding-collector
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# One-shot collection from all 8 exchanges&lt;/span&gt;
funding-collector &lt;span class="nt"&gt;--exchanges&lt;/span&gt; binance,bybit,okx,bitget,mexc,hyperliquid,gateio,dydx

&lt;span class="c"&gt;# Or run as a daemon&lt;/span&gt;
funding-collector &lt;span class="nt"&gt;--loop&lt;/span&gt; 300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you the same data layer that powers the hosted service, MIT-licensed, on your own infrastructure. The hosted service then becomes optional — you'd only pay if you want the API/dashboard/alerts wrapper without the operational overhead.&lt;/p&gt;

&lt;p&gt;The OSS package is ~700 lines of Python. You can read the entire codebase in 30 minutes. There's no magic, no plugin system, no async runtime. Just seven &lt;code&gt;fetch_*&lt;/code&gt; functions and one SQLite schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you give up by migrating
&lt;/h2&gt;

&lt;p&gt;Honest disclosure of the trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smaller exchange coverage.&lt;/strong&gt; 8 vs ~30. If you trade alts on Bitfinex, BingX, Phemex, etc., they're not in Funding Finder yet. Open a PR if you want them faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less historical data on the entry tier.&lt;/strong&gt; 30 days vs 180 days. Pro tier (€15/mo) gets 12 months but it's still less than Coinglass's higher tiers offer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No liquidations / ETF / on-chain.&lt;/strong&gt; Funding Finder is funding-rate focused. If you use Coinglass for the broader derivatives surface, stay on Coinglass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newer product.&lt;/strong&gt; Funding Finder is months old, not years. The track record is shorter. The road map is clearer than the path actually taken.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you gain
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6x cheaper entry tier.&lt;/strong&gt; €5 vs $29.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;20x rate limit.&lt;/strong&gt; 600 req/min vs 30.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No signup for casual use.&lt;/strong&gt; Free tier needs no account, no email, no API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slippage-aware net APY&lt;/strong&gt; built into the arbitrage endpoint. Coinglass shows you gross spreads; Funding Finder also computes the net yield after estimated slippage and fees for a given notional size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-source self-host option.&lt;/strong&gt; Run the data layer on your own VPS for free if you want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A focused, simple API surface&lt;/strong&gt; instead of the broad surface you're not using.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Migrating tools is annoying. The cost of the migration is real and the benefit is incremental. For Funding Finder vs Coinglass, the migration is worth it if and only if you're a funding-rate-focused user who finds the $29/month painful. For everyone else, stay on Coinglass.&lt;/p&gt;

&lt;p&gt;I built Funding Finder because I am that specific user. If you're reading this and recognizing yourself, the free tier is at &lt;a href="https://foxyyy.com/" rel="noopener noreferrer"&gt;https://foxyyy.com/&lt;/a&gt;. Take 10 minutes, run the three sample API calls, and decide for yourself.&lt;/p&gt;

&lt;p&gt;Comparison page (more detail): &lt;a href="https://foxyyy.com/vs/coinglass" rel="noopener noreferrer"&gt;https://foxyyy.com/vs/coinglass&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;API docs: &lt;a href="https://foxyyy.com/docs" rel="noopener noreferrer"&gt;https://foxyyy.com/docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OSS data collector: &lt;a href="https://github.com/clementslowik/funding-collector" rel="noopener noreferrer"&gt;github.com/clementslowik/funding-collector&lt;/a&gt; (MIT license)&lt;/p&gt;

&lt;p&gt;— Clément&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>python</category>
      <category>api</category>
      <category>trading</category>
    </item>
    <item>
      <title>Seven crypto exchanges, one normalized schema, ~700 lines of Python</title>
      <dc:creator>FoxyyyBusiness</dc:creator>
      <pubDate>Fri, 10 Apr 2026 10:35:14 +0000</pubDate>
      <link>https://forem.com/foxyyybusiness/seven-crypto-exchanges-one-normalized-schema-700-lines-of-python-9p9</link>
      <guid>https://forem.com/foxyyybusiness/seven-crypto-exchanges-one-normalized-schema-700-lines-of-python-9p9</guid>
      <description>&lt;p&gt;This is a follow-up to &lt;a href="https://dev.toTODO-link-after-publish"&gt;my previous post on building a funding-rate arbitrage scanner&lt;/a&gt;. That post was about the &lt;em&gt;product&lt;/em&gt; — what it does, the three non-obvious gotchas, and why I built it. This one is about the &lt;em&gt;plumbing&lt;/em&gt;: how seven different exchange APIs handle the same data and what it took to unify them.&lt;/p&gt;

&lt;p&gt;If you've ever thought "I'll just call the public APIs and join the data, how hard can it be" — this post is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dataset I wanted
&lt;/h2&gt;

&lt;p&gt;For each USDT-margined perpetual on each major venue, I needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Current funding rate&lt;/strong&gt; (per period, decimal — e.g. &lt;code&gt;0.0001&lt;/code&gt; = 0.01%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Funding interval&lt;/strong&gt; in hours (8h, 4h, 1h depending on the venue and the symbol)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mark price&lt;/strong&gt; (for sizing calculations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;24h volume in USD&lt;/strong&gt; (for liquidity filtering — without this, the scanner is useless)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next funding time&lt;/strong&gt; (UNIX seconds — for "this opportunity expires in X minutes" UI)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sounds simple. It's not. Each exchange returns a subset of this in a different shape, and you usually need at least two API calls per exchange to assemble the full record. Here's how each one works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Binance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strategy&lt;/strong&gt;: two bulk calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;fapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;binance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;fapi&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;premiumIndex&lt;/span&gt;
&lt;span class="c1"&gt;# Returns ALL symbols with: lastFundingRate, markPrice, nextFundingTime
# One call, ~670 USDT-M symbols, instant.
&lt;/span&gt;
&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;fapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;binance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;fapi&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="n"&gt;hr&lt;/span&gt;
&lt;span class="c1"&gt;# Returns ALL symbols with: quoteVolume (24h USDT volume)
# One call, same coverage, instant.
&lt;/span&gt;
&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;fapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;binance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;fapi&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;fundingInfo&lt;/span&gt;
&lt;span class="c1"&gt;# Returns the per-symbol fundingIntervalHours, but ONLY for symbols
# whose interval is non-default (4h, 1h). Default 8h symbols are omitted.
# Cache once, refresh weekly.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Binance is the cleanest. The funding interval endpoint returning only non-default symbols is mildly annoying (you have to assume 8h if a symbol is missing) but that's a minor wart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bybit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strategy&lt;/strong&gt;: bulk tickers + paginated instruments-info.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bybit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v5&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tickers&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;linear&lt;/span&gt;
&lt;span class="c1"&gt;# Returns ALL linear perps with: fundingRate, markPrice, turnover24h (USD), nextFundingTime
# One call, ~544 symbols.
&lt;/span&gt;
&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bybit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v5&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;instruments&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="c1"&gt;# Returns funding interval in MINUTES (not hours, not seconds). Convert.
# Paginated, but limit=1000 fits everything in 1 call usually.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bybit is also clean once you know about the unit (minutes for &lt;code&gt;fundingInterval&lt;/code&gt;, USD for &lt;code&gt;turnover24h&lt;/code&gt;). The pagination cursor is technically required but in practice you fit everything in 1 page.&lt;/p&gt;

&lt;h2&gt;
  
  
  OKX
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strategy&lt;/strong&gt;: bulk tickers + per-instrument funding rate (parallelized).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;okx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v5&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;instruments&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;instType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SWAP&lt;/span&gt;
&lt;span class="c1"&gt;# Returns the list of SWAP instruments. Filter by settleCcy=USDT.
# ~285 USDT-margined SWAPs.
&lt;/span&gt;
&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;okx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v5&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tickers&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;instType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SWAP&lt;/span&gt;
&lt;span class="c1"&gt;# Returns volume + last price per instrument. One bulk call.
&lt;/span&gt;
&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;okx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v5&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;funding&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;instId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BTC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;USDT&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;SWAP&lt;/span&gt;
&lt;span class="c1"&gt;# Returns funding rate, fundingTime, nextFundingTime for ONE instrument.
# Yes, one. Per. Call. There is no bulk endpoint for current funding rate at OKX.
# Public rate limit: ~20 req/2s.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where it gets ugly. OKX has the most awkward API of the major venues. To get current funding rates for 285 symbols you have to make 285 sequential calls (or 8 parallel workers respecting the rate limit). 285 calls × ~0.3s each ≈ 85 seconds sequential, 8 seconds parallelized.&lt;/p&gt;

&lt;p&gt;The funding interval is computed from &lt;code&gt;fundingTime - prevFundingTime&lt;/code&gt; because OKX doesn't expose it as a field. That works as long as both fields are present, which they are 99.9% of the time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;as_completed&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;inst&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;usdt_swaps&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;as_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8 workers stays comfortably under the 10 req/s limit. 0 failures in production for the past day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bitget
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strategy&lt;/strong&gt;: two bulk calls. The cleanest of all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bitget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mix&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tickers&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;productType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;usdt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;
&lt;span class="c1"&gt;# Returns ALL contracts: fundingRate, markPrice, usdtVolume, holdingAmount
# One call, ~537 symbols.
&lt;/span&gt;
&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bitget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mix&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;fund&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;productType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;usdt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;
&lt;span class="c1"&gt;# Returns fundingRateInterval (hours, integer) per symbol.
# One call.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bitget gets a gold star. Two calls, both bulk, fields named clearly, units obvious. If only every exchange were like this.&lt;/p&gt;

&lt;h2&gt;
  
  
  MEXC
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strategy&lt;/strong&gt;: two bulk calls, parallel-friendly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mexc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;funding_rate&lt;/span&gt;
&lt;span class="c1"&gt;# Returns ALL symbols with: fundingRate, collectCycle (hours), nextSettleTime
# One call, ~762 symbols.
&lt;/span&gt;
&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mexc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;
&lt;span class="c1"&gt;# Returns ALL symbols with: fairPrice, amount24 (USDT volume)
# One call.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MEXC's &lt;code&gt;collectCycle&lt;/code&gt; is in hours (clean) and they expose every field on bulk endpoints. Symbol format is &lt;code&gt;BTC_USDT&lt;/code&gt; instead of &lt;code&gt;BTCUSDT&lt;/code&gt;, which requires a small normalization step. Otherwise, also a gold star.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate.io
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strategy&lt;/strong&gt;: two bulk calls + an in-delisting filter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gateio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v4&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usdt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;contracts&lt;/span&gt;
&lt;span class="c1"&gt;# Returns funding_interval (in SECONDS, divide by 3600), in_delisting flag, name.
# One call, ~642 symbols.
&lt;/span&gt;
&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gateio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v4&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usdt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tickers&lt;/span&gt;
&lt;span class="c1"&gt;# Returns funding_rate, mark_price, volume_24h_quote (USDT volume), contract.
# One call.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gate.io is fine but has the most variety of unit conventions in a single response: &lt;code&gt;funding_interval&lt;/code&gt; is in seconds, &lt;code&gt;volume_24h_quote&lt;/code&gt; is in USD, &lt;code&gt;funding_rate&lt;/code&gt; is decimal. The &lt;code&gt;in_delisting&lt;/code&gt; flag is critical — without it you'll see violently spiking funding rates on coins that are about to disappear, which look like 5000% APY opportunities until you realize you can't trade them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hyperliquid
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strategy&lt;/strong&gt;: ONE bulk POST call. Yes, POST, not GET.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;POST&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hyperliquid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xyz&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;
&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metaAndAssetCtxs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns a 2-element array:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[0].universe&lt;/code&gt; — list of perp assets with their &lt;code&gt;name&lt;/code&gt; (just the base, e.g. &lt;code&gt;BTC&lt;/code&gt;, no quote suffix)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[1]&lt;/code&gt; — parallel array of contexts with &lt;code&gt;funding&lt;/code&gt;, &lt;code&gt;markPx&lt;/code&gt;, &lt;code&gt;dayNtlVlm&lt;/code&gt; (USD volume)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Single call, 229 symbols, instant. Funding interval is hardcoded 1h (Hyperliquid's entire venue is 1-hour funding).&lt;/p&gt;

&lt;p&gt;The catch: it's a JSON-RPC-ish POST API with parallel arrays as the response shape. If you're used to REST GET endpoints, the first time you see this you'll waste 10 minutes wondering why your &lt;code&gt;params={}&lt;/code&gt; doesn't work. POST + JSON body, that's the trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unified output
&lt;/h2&gt;

&lt;p&gt;After all that, every exchange's collector function returns the same dict shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;binance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BTCUSDT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# normalized to &amp;lt;BASE&amp;gt;USDT
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;funding_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.00004&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;funding_interval_hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next_funding_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1775606400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mark_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;65000.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume_24h_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1234567.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetched_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1775597628&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These get inserted via &lt;code&gt;INSERT OR REPLACE&lt;/code&gt; into the single SQLite table. The composite primary key &lt;code&gt;(exchange, symbol, fetched_at)&lt;/code&gt; means every collector cycle is preserved as history.&lt;/p&gt;

&lt;p&gt;7 exchanges, 3700+ symbols, ~12 seconds total per cycle. Most of that is the OKX parallelized fetch (8s) — Binance, Bybit, Bitget, MEXC, Gate.io are sub-second each, Hyperliquid is sub-second, and they all run in series (could parallelize them too, easy win).&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;There is no industry standard for funding-rate data&lt;/strong&gt;, even for the most-traded instrument class in crypto. Each exchange invents its own field names, units, endpoint shapes, and pagination conventions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "single bulk call returning everything" pattern is rare.&lt;/strong&gt; Most exchanges require 2-3 calls to assemble the full record (rate + volume + interval). OKX requires N+2 calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Units matter more than you think.&lt;/strong&gt; Hours, minutes, seconds, milliseconds, decimals, percentages — every exchange picks at least one unit that surprises you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;in_delisting&lt;/code&gt; flags exist on some venues and not others.&lt;/strong&gt; Without them, you'll publish fake opportunities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;1000&lt;/code&gt;-multiplier coins (1000PEPE, 1000SHIB, 1000XEC) are a nightmare&lt;/strong&gt; because their cross-exchange equivalence is non-trivial. I filter them out for v0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The library is on GitHub at &lt;code&gt;&amp;lt;TBD-after-push&amp;gt;&lt;/code&gt; under MIT. It's ~700 lines of Python, no async, no clever metaprogramming, no plugin system. Just seven &lt;code&gt;fetch_*&lt;/code&gt; functions and one SQLite schema.&lt;/p&gt;

&lt;p&gt;If you want to add an 8th exchange, the contract is documented in &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;. The first PR I'd love to merge is &lt;strong&gt;dYdX v4&lt;/strong&gt; — their REST API is cleaner than most CEXes and I just haven't written it yet.&lt;/p&gt;

&lt;p&gt;Live scanner using this library: &lt;strong&gt;&lt;a href="https://foxyyy.com/" rel="noopener noreferrer"&gt;https://foxyyy.com/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;API docs: &lt;strong&gt;&lt;a href="https://foxyyy.com/docs" rel="noopener noreferrer"&gt;https://foxyyy.com/docs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>crypto</category>
      <category>api</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a free 7-exchange funding-rate arbitrage scanner because I refused to pay $29/month for one</title>
      <dc:creator>FoxyyyBusiness</dc:creator>
      <pubDate>Fri, 10 Apr 2026 10:29:03 +0000</pubDate>
      <link>https://forem.com/foxyyybusiness/i-built-a-free-7-exchange-funding-rate-arbitrage-scanner-because-i-refused-to-pay-29month-for-one-17c0</link>
      <guid>https://forem.com/foxyyybusiness/i-built-a-free-7-exchange-funding-rate-arbitrage-scanner-because-i-refused-to-pay-29month-for-one-17c0</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://foxyyy.com/" rel="noopener noreferrer"&gt;Funding Finder&lt;/a&gt;, a free cross-exchange perpetual funding-rate arbitrage scanner. It polls &lt;strong&gt;Binance, Bybit, OKX, Bitget, MEXC, Hyperliquid, and Gate.io&lt;/strong&gt; every 5 minutes — about 3700 USDT-margined perpetuals — ranks every base coin by the spread between its cheapest long leg and most expensive short leg, annualizes correctly per-symbol, and filters out illiquid noise.&lt;/p&gt;

&lt;p&gt;Free web view, free JSON API, no signup. The OSS data collector is on GitHub under MIT.&lt;/p&gt;

&lt;p&gt;This post is about the &lt;strong&gt;three non-obvious things most existing tools get wrong&lt;/strong&gt; and how I handled each one. If you trade funding-rate arbitrage even casually, two of them have probably been silently distorting your scanner output.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I trade a small funding-arb book on the side. The data ergonomics for this strategy are surprisingly bad.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coinglass&lt;/strong&gt; has the best UI but the entry API plan starts at $29/month with 30 req/min and a 180-day history cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CoinAPI&lt;/strong&gt; is commercial and pitched at institutions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coinalyze&lt;/strong&gt; is free but BTC-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each exchange's native API&lt;/strong&gt; is free, but you have to scrape, normalize symbols, and reconcile funding intervals per venue. By the time you've done that, you've built half the product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The strategy itself is dead simple: long the perp on the exchange with the lowest funding rate, short the perp on the exchange with the highest funding rate. Both legs are perp, no spot, beta-neutral. You capture the funding spread until convergence.&lt;/p&gt;

&lt;p&gt;What I needed was a no-bullshit table that ranks current cross-exchange spreads by annualized yield, filtered by liquidity, across all the major venues. So I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture (deliberately boring)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;collector.py  →  funding.db (SQLite + WAL)  →  api.py (Flask)  →  static/index.html
     ↑
     │
     5-min loop, polls 7 exchange public APIs in parallel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No Kafka, no Redis, no microservices, no Docker stack. One SQLite file, one Flask process, one HTML file. The whole thing fits in ~700 lines of Python and runs on a $5 VPS.&lt;/p&gt;

&lt;p&gt;The schema is one table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;funding_rates&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;exchange&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;funding_rate&lt;/span&gt; &lt;span class="nb"&gt;REAL&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;funding_interval_hours&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;next_funding_time&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mark_price&lt;/span&gt; &lt;span class="nb"&gt;REAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;volume_24h_usd&lt;/span&gt; &lt;span class="nb"&gt;REAL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fetched_at&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fetched_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The composite primary key gives you a free historical record. Run the collector in a 5-minute loop and after a week you have a queryable funding-rate history per (exchange, symbol).&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha #1 — funding interval is not always 8 hours
&lt;/h2&gt;

&lt;p&gt;Most tools you find on GitHub assume an 8-hour funding interval everywhere. They compute annualized yield as &lt;code&gt;funding_rate × 3 × 365&lt;/code&gt; (3 funding settlements per day × 365 days).&lt;/p&gt;

&lt;p&gt;That's wrong.&lt;/p&gt;

&lt;p&gt;Binance has &lt;strong&gt;425 USDT-margined perps on a 4-hour cycle and 4 perps on a 1-hour cycle&lt;/strong&gt;, in addition to the ~241 standard 8-hour ones. Bybit has &lt;strong&gt;377 on 4-hour and 3 on 1-hour&lt;/strong&gt;. Bitget and MEXC each have a similar spread. Hyperliquid is &lt;strong&gt;entirely on a 1-hour cycle&lt;/strong&gt;. That's roughly &lt;strong&gt;30% of the available CEX market&lt;/strong&gt; (and 100% of Hyperliquid) that gets misannualized if you assume 8h.&lt;/p&gt;

&lt;p&gt;A 0.05% per-period funding rate is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;≈ 55% APY on an 8h symbol (×3×365)&lt;/li&gt;
&lt;li&gt;≈ 109% APY on a 4h symbol (×6×365)&lt;/li&gt;
&lt;li&gt;≈ 438% APY on a 1h symbol (×24×365)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your scanner shows the same annualized number for all of them, your scanner is lying to you.&lt;/p&gt;

&lt;p&gt;The fix is to fetch the per-symbol funding interval at startup and cache it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_binance_funding_intervals&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://fapi.binance.com/fapi/v1/fundingInfo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fundingIntervalHours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Bybit it's &lt;code&gt;/v5/market/instruments-info?category=linear&lt;/code&gt;, paginated, with &lt;code&gt;fundingInterval&lt;/code&gt; in &lt;strong&gt;minutes&lt;/strong&gt;. Convert to hours, cache, you're done. For Bitget it's in &lt;code&gt;/api/v2/mix/market/current-fund-rate&lt;/code&gt;. For MEXC it's &lt;code&gt;collectCycle&lt;/code&gt; in the funding-rate response. For Gate.io it's in seconds in &lt;code&gt;/futures/usdt/contracts&lt;/code&gt;. For Hyperliquid it's hardcoded to 1 (their entire venue is 1h).&lt;/p&gt;

&lt;p&gt;Five exchanges, five subtly different conventions for the same field. Welcome to crypto data plumbing.&lt;/p&gt;

&lt;p&gt;This single fix changed the top-10 of my own table substantially. The "best" 8h-symbol opportunity at the top before the fix was actually middling once I corrected the 4h-symbols above it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha #2 — the liquidity filter is the actual product
&lt;/h2&gt;

&lt;p&gt;Before the liquidity filter, my scanner found 279 cross-exchange opportunities at any given time. Sounds great.&lt;/p&gt;

&lt;p&gt;It wasn't. The top 10 were a parade of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Coins about to be delisted (one exchange's funding swings violently as market makers hedge their unwind)&lt;/li&gt;
&lt;li&gt;New listings with thin order books on one of the two legs&lt;/li&gt;
&lt;li&gt;Tokens with $40k of daily volume — your $5k position would move the mark price by 2% on entry alone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding &lt;code&gt;min_volume_24h_usd&lt;/code&gt; as a both-legs filter brought 279 → 51 at $5M, → 6 at $50M. The 6 at $50M+ are the actually-tradeable ones. Everything else is bait.&lt;/p&gt;

&lt;p&gt;Code is trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;liquid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume_24h_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;min_volume&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;liquid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the realization that the scanner is &lt;strong&gt;only as good as the liquidity floor it imposes&lt;/strong&gt; is the part nobody seems to talk about. It's not a feature of "advanced" scanners, it's the default that should ship.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha #3 — symbol normalization matters more than you think
&lt;/h2&gt;

&lt;p&gt;Each exchange has its own naming convention. Binance and Bybit use &lt;code&gt;BTCUSDT&lt;/code&gt;. OKX uses &lt;code&gt;BTC-USDT-SWAP&lt;/code&gt;. Bitget uses &lt;code&gt;BTCUSDT&lt;/code&gt;. MEXC uses &lt;code&gt;BTC_USDT&lt;/code&gt;. Gate.io uses &lt;code&gt;BTC_USDT&lt;/code&gt;. Hyperliquid uses just &lt;code&gt;BTC&lt;/code&gt; (no quote suffix because everything is USD-margined intrinsically).&lt;/p&gt;

&lt;p&gt;If you join naively, you'll miss half your cross-exchange opportunities. Worse: if you don't know that Binance's &lt;code&gt;1000PEPEUSDT&lt;/code&gt; is the same coin as MEXC's &lt;code&gt;PEPE_USDT&lt;/code&gt; (different multiplier), you'll compute a "spread" that's completely fake.&lt;/p&gt;

&lt;p&gt;The simple normalization rules for the 90% case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BTCUSDT&lt;/code&gt; → base &lt;code&gt;BTC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BTC-USDT-SWAP&lt;/code&gt; → strip &lt;code&gt;-USDT-SWAP&lt;/code&gt;, base &lt;code&gt;BTC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BTC_USDT&lt;/code&gt; → strip &lt;code&gt;_USDT&lt;/code&gt;, base &lt;code&gt;BTC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BTC&lt;/code&gt; (Hyperliquid) → base &lt;code&gt;BTC&lt;/code&gt;, append &lt;code&gt;USDT&lt;/code&gt; for the joined symbol&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the 10% edge cases (1000-multiplier coins, 10000-multiplier coins, exotic pairs), you either build a manual mapping or filter them out. I filter them out for v0 — better to under-report than to publish fake spreads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_base_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;okx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-USDT-SWAP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removesuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-USDT-SWAP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mexc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gateio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_USDT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USDT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sym&lt;/span&gt;  &lt;span class="c1"&gt;# Hyperliquid: name is already the base
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;dYdX v4 and Coinbase Derivatives&lt;/strong&gt; to round out the universe of major venues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent history endpoint&lt;/strong&gt; is already live (&lt;code&gt;/api/funding/history/&amp;lt;base&amp;gt;?hours=720&lt;/code&gt;) — the rolling collection populates SQLite, the endpoint just exposes it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram alert bot&lt;/strong&gt;, free, when an opportunity above a user-defined yield + liquidity floor appears&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API key system&lt;/strong&gt; with a paid tier under $10/month for full history (12 months+) and higher rate limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slippage estimation&lt;/strong&gt; using order book depth, so the displayed APY accounts for the entry/exit cost on each leg&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source for the OSS collector + schema is on GitHub at &lt;code&gt;&amp;lt;TBD-after-push&amp;gt;&lt;/code&gt; under MIT. The API/dashboard layer stays as a hosted service.&lt;/p&gt;

&lt;p&gt;Try it: &lt;strong&gt;&lt;a href="https://foxyyy.com/" rel="noopener noreferrer"&gt;https://foxyyy.com/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;API docs: &lt;strong&gt;&lt;a href="https://foxyyy.com/docs" rel="noopener noreferrer"&gt;https://foxyyy.com/docs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tell me what's broken. Especially if you trade this strategy live — I want to hear about every gotcha that bit you, because the difference between a paper backtest and a real position is all in the details (settlement timing, mark-price gaps at funding-time, withdraw delays, leg correlation breakdown). I'll write a follow-up post on that once I've burned myself a few times.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built solo. No funding round, no marketing budget, no enterprise sales team. Just one developer who got tired of paying for data that should be in the public domain anyway.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>python</category>
      <category>api</category>
      <category>trading</category>
    </item>
    <item>
      <title>5 systemd units for a Python web app, complete and copy-pasteable. No Docker.</title>
      <dc:creator>FoxyyyBusiness</dc:creator>
      <pubDate>Fri, 10 Apr 2026 10:28:59 +0000</pubDate>
      <link>https://forem.com/foxyyybusiness/5-systemd-units-for-a-python-web-app-complete-and-copy-pasteable-no-docker-5abg</link>
      <guid>https://forem.com/foxyyybusiness/5-systemd-units-for-a-python-web-app-complete-and-copy-pasteable-no-docker-5abg</guid>
      <description>&lt;p&gt;This post is a practical reference for the dev who's tired of Dockerfile + docker-compose.yml + nginx config + Watchtower for a personal project. Everything below works on a stock Ubuntu/Debian VPS with no extra tooling beyond &lt;code&gt;python3&lt;/code&gt; and &lt;code&gt;pip&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://foxyyy.com/" rel="noopener noreferrer"&gt;Funding Finder&lt;/a&gt; on this exact setup: 5 systemd services, 1 Python interpreter, ~70 MB resident memory total, $5/month VPS. Up for over 24 hours with zero issues at the time of writing.&lt;/p&gt;

&lt;p&gt;The 5 unit files are: API server, background collector loop, healthcheck monitor, alerts worker, mission supervision dashboard. They're all independent processes managed by systemd's &lt;code&gt;Restart=always&lt;/code&gt;, log to disk, survive reboots, and start automatically on boot.&lt;/p&gt;

&lt;p&gt;Copy-paste each into &lt;code&gt;/etc/systemd/system/&amp;lt;name&amp;gt;.service&lt;/code&gt; and run &lt;code&gt;systemctl enable --now &amp;lt;name&amp;gt;&lt;/code&gt; to activate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit 1 — Flask API server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Funding Finder API server&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/root/project_30d/artifacts/funding_finder&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3 /root/project_30d/artifacts/funding_finder/api.py&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;append:/root/project_30d/artifacts/funding_finder/data/api.log&lt;/span&gt;
&lt;span class="py"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;append:/root/project_30d/artifacts/funding_finder/data/api.log&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PORT=8083&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Flask dev server is fine for personal projects up to ~500 req/sec on a single core. If you need more, swap &lt;code&gt;ExecStart&lt;/code&gt; for &lt;code&gt;gunicorn -w 4 -b 0.0.0.0:8083 api:app&lt;/code&gt;. Same unit file, one line different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit 2 — Background data collector loop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Funding Finder data collector (5-min loop)&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/root/project_30d/artifacts/funding_finder&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3 /root/project_30d/artifacts/funding_finder/collector.py --exchanges binance,bybit,okx,bitget,mexc,hyperliquid,gateio,dydx --loop 300&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;append:/root/project_30d/artifacts/funding_finder/data/collector.log&lt;/span&gt;
&lt;span class="py"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;append:/root/project_30d/artifacts/funding_finder/data/collector.log&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The collector itself runs &lt;code&gt;while True: collect_all(); time.sleep(300)&lt;/code&gt;. systemd's &lt;code&gt;Restart=always&lt;/code&gt; handles process crashes — if Python segfaults or hits an unhandled exception that escapes the loop, systemd brings it back in 10 seconds.&lt;/p&gt;

&lt;p&gt;For a slower cycle (say, 10 minutes), change &lt;code&gt;--loop 300&lt;/code&gt; to &lt;code&gt;--loop 600&lt;/code&gt; and &lt;code&gt;systemctl daemon-reload &amp;amp;&amp;amp; systemctl restart funding-finder-collector&lt;/code&gt;. No code change needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit 3 — Healthcheck monitor with Telegram alerts
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Funding Finder healthcheck monitor (Telegram alerts)&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target funding-finder-api.service&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;funding-finder-api.service&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/root/project_30d/artifacts/funding_finder&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3 /root/project_30d/artifacts/funding_finder/monitor.py --interval 600 --alert-after 2&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;
&lt;span class="py"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;append:/root/project_30d/artifacts/funding_finder/data/monitor.log&lt;/span&gt;
&lt;span class="py"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;append:/root/project_30d/artifacts/funding_finder/data/monitor.log&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Wants=&lt;/code&gt; directive means: if the API service is stopped, this monitor will be too (but won't fail). The &lt;code&gt;After=&lt;/code&gt; directive ensures the monitor starts after the API on boot, so it doesn't immediately alert about a "missing" API that's still booting.&lt;/p&gt;

&lt;p&gt;The monitor itself polls &lt;code&gt;/api/health&lt;/code&gt; every 10 minutes and sends a Telegram message after 2 consecutive failures. ~150 lines of Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit 4 — Alerts worker (background dispatch)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Funding Finder alerts worker (Telegram dispatch)&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target funding-finder-api.service&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;funding-finder-api.service&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/root/project_30d/artifacts/funding_finder&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3 /root/project_30d/artifacts/funding_finder/alerts_worker.py --interval 60&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;15&lt;/span&gt;
&lt;span class="py"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;append:/root/project_30d/artifacts/funding_finder/data/alerts.log&lt;/span&gt;
&lt;span class="py"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;append:/root/project_30d/artifacts/funding_finder/data/alerts.log&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the user-facing "alerts" feature that the paid tier uses. 60-second polling cycle, scans the database for active alerts, sends matching Telegram messages with cooldown enforcement. ~200 lines of Python.&lt;/p&gt;

&lt;p&gt;I wrote a &lt;a href="https://dev.toTODO-link"&gt;separate post on the architecture of this worker&lt;/a&gt; — short version, you don't need a queue or websockets at this scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unit 5 — Mission supervision dashboard
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Mission 30j supervision dashboard&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/root/project_30d&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3 /root/project_30d/dashboard/backend.py&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;append:/root/project_30d/journal/backend.log&lt;/span&gt;
&lt;span class="py"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;append:/root/project_30d/journal/backend.log&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PORT=8082&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A second Flask service for a different concern (mission supervision dashboard). Same template, different working directory and port.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to deploy all 5 in one shot
&lt;/h2&gt;

&lt;p&gt;Save the 5 files above into &lt;code&gt;/etc/systemd/system/&lt;/code&gt;, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; funding-finder-api
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; funding-finder-collector
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; funding-finder-monitor
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; funding-finder-alerts
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; project30d-dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five commands. Done. All five services are running, will restart on crash, will start on boot, and are logging to files you can &lt;code&gt;tail -f&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl is-active funding-finder-&lt;span class="o"&gt;{&lt;/span&gt;api,collector,monitor,alerts&lt;span class="o"&gt;}&lt;/span&gt; project30d-dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To restart everything (e.g. after a code change):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart funding-finder-&lt;span class="o"&gt;{&lt;/span&gt;api,collector,monitor,alerts&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To stop everything (e.g. for maintenance):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop funding-finder-&lt;span class="o"&gt;{&lt;/span&gt;api,collector,monitor,alerts&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you DON'T need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Dockerfile&lt;/strong&gt; — your code runs in the host Python, no isolation layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No docker-compose.yml&lt;/strong&gt; — systemd is the orchestrator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No nginx in front&lt;/strong&gt; — Flask listens on the public port directly (or use ufw to control which IPs can connect)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No process manager (supervisor, pm2, etc)&lt;/strong&gt; — systemd is the process manager&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No log shipper (filebeat, fluentd, etc)&lt;/strong&gt; — &lt;code&gt;tail&lt;/code&gt; is enough at small scale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No image registry&lt;/strong&gt; — there are no images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No CI/CD pipeline&lt;/strong&gt; — &lt;code&gt;git pull &amp;amp;&amp;amp; systemctl restart&lt;/code&gt; is your deploy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No staging environment&lt;/strong&gt; — fix it in prod, you have an audience of 0 right now&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No "production-grade" web server&lt;/strong&gt; — Flask dev server handles ~500 req/sec on 1 core, plenty for personal projects&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When you DO need more
&lt;/h2&gt;

&lt;p&gt;Each of those things makes sense at different scales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; when you need to ship the same app to multiple environments with different OS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nginx&lt;/strong&gt; when you need TLS termination, rate limiting, or static file caching at the edge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gunicorn&lt;/strong&gt; when you exceed ~500 req/sec sustained&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real DB&lt;/strong&gt; when SQLite WAL can't handle the write contention (rarely below 50k writes/sec)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A staging environment&lt;/strong&gt; when you have paying customers whose downtime would cost real money&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A CI pipeline&lt;/strong&gt; when there's more than one developer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Until each of those triggers fires, the boring stack is the right answer. Stop pre-optimizing for problems you don't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on &lt;code&gt;Restart=always&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the magic line. Without it, a Python crash means the service stays dead until you SSH in and notice. With it, you get automatic recovery from 95% of failure modes for free. The other 5% (corrupted database, full disk, etc) require you to actually fix the underlying problem, but for "the process died because of an unhandled exception" or "OOM killer reaped it", systemd's restart is enough.&lt;/p&gt;

&lt;p&gt;Combined with &lt;code&gt;RestartSec=5&lt;/code&gt; (or 10, or 30 — pick based on the cost of a fast restart loop), you get a deployment that's harder to break than 90% of the Kubernetes setups I've seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you're running a personal project on a Docker Compose stack right now and feeling the operational weight of it, try this exercise: rewrite ONE of your services as a systemd unit. It's a 1-hour project. You'll either love it or you'll discover you actually need the Docker isolation for some specific reason.&lt;/p&gt;

&lt;p&gt;Most of the time, you'll love it.&lt;/p&gt;

&lt;p&gt;The full source for the 5 unit files above is in the &lt;a href="https://foxyyy.com/" rel="noopener noreferrer"&gt;Funding Finder repo&lt;/a&gt; (which is the project they run). Live tool that uses them: &lt;a href="https://foxyyy.com/" rel="noopener noreferrer"&gt;https://foxyyy.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;— Clément&lt;/p&gt;

</description>
      <category>python</category>
      <category>systemd</category>
      <category>devops</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Three crypto exchange volume bugs that were hiding in plain sight</title>
      <dc:creator>FoxyyyBusiness</dc:creator>
      <pubDate>Thu, 09 Apr 2026 23:03:30 +0000</pubDate>
      <link>https://forem.com/foxyyybusiness/three-crypto-exchange-volume-bugs-that-were-hiding-in-plain-sight-5hho</link>
      <guid>https://forem.com/foxyyybusiness/three-crypto-exchange-volume-bugs-that-were-hiding-in-plain-sight-5hho</guid>
      <description>&lt;p&gt;I run a service that pulls funding rates from 16 perpetual futures exchanges every five minutes and exposes a unified API for cross-venue arbitrage. Each fetcher is ~50 lines of Python — request, parse, normalize, store. Boring stuff, in theory.&lt;/p&gt;

&lt;p&gt;In practice, three of those 50-line fetchers were silently broken in ways that produced &lt;em&gt;plausible-looking&lt;/em&gt; numbers. My unit tests passed. The data looked sane. But when I added a simple &lt;code&gt;/api/funding/by_volume&lt;/code&gt; endpoint that ranks base coins by their &lt;strong&gt;total cross-exchange 24h dollar volume&lt;/strong&gt;, the leaderboard came back with &lt;code&gt;SATS&lt;/code&gt; showing $180 trillion in daily turnover.&lt;/p&gt;

&lt;p&gt;That number is roughly the entire global stock-and-bond market, traded in one day, in one shitcoin, on perpetual futures. Something was off.&lt;/p&gt;

&lt;p&gt;Here are the three bugs I dug out, in the order I found them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #1 — OKX is denominated in BASE coin, not USDT
&lt;/h2&gt;

&lt;p&gt;OKX's &lt;code&gt;/api/v5/market/tickers?instType=SWAP&lt;/code&gt; endpoint returns a &lt;code&gt;volCcy24h&lt;/code&gt; field for every perp. The name reads like "volume in counter currency" — i.e. the quote, USDT. That's how Binance, Bybit, Bitget, Gate.io, MEXC, and most others structure it.&lt;/p&gt;

&lt;p&gt;OKX does not. For SWAPs, &lt;code&gt;volCcy24h&lt;/code&gt; is in &lt;strong&gt;base coin units&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So my fetcher was treating &lt;code&gt;138000&lt;/code&gt; (138k BTC traded in 24h, which is correct) as &lt;code&gt;$138k in USD volume&lt;/code&gt;. OKX BTC volume came back as $136k against Binance's $16.5B. I had been staring at this in the dashboard for two days without noticing because BTC was already at the top of the list — &lt;em&gt;the ranking was right, the magnitude was off by five orders of magnitude&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The fix is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_okx_tickers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.okx.com/api/v5/market/tickers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;instType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SWAP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]:&lt;/span&gt;
        &lt;span class="n"&gt;inst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;instId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;base_vol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volCcy24h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;last&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# volCcy24h is in BASE coin units for SWAPs — multiply by last price for USD.
&lt;/span&gt;        &lt;span class="n"&gt;vol_usd&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_vol&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vol_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vol_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the fix, OKX BTC volume jumped from $136k to $9.7B. That matches every other public reporter (CoinGecko, Coinglass, etc).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: field names are descriptions, not contracts. Read the docs, but trust the cross-exchange comparison even more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #2 — BTSE counts CONTRACTS, not coins
&lt;/h2&gt;

&lt;p&gt;BTSE's &lt;code&gt;/futures/api/v2.1/market_summary&lt;/code&gt; returns a &lt;code&gt;volume&lt;/code&gt; field for each market. It is, like everywhere else, a number. But it's not a number of base coins. It's a number of &lt;em&gt;contracts&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A contract on BTSE has a configurable &lt;code&gt;contractSize&lt;/code&gt;. For BTC perps, the contract size is &lt;code&gt;0.001&lt;/code&gt;. For some alts, it's &lt;code&gt;1&lt;/code&gt;. For some new perps, it's a weird fraction like &lt;code&gt;0.0001&lt;/code&gt;. The &lt;code&gt;volume&lt;/code&gt; field is the count of contracts traded — and the actual notional in base coin units is &lt;code&gt;volume * contractSize&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had defaulted my BTSE fetcher to assuming one contract = one base coin (a reasonable Binance-shaped assumption). Result: BTSE BTC was reporting $60 trillion of 24h volume, because I was interpreting 1 million contracts (= 1000 BTC) as 1 million BTC.&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;vol_contracts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;contract_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contractSize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;vol_base_units&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vol_contracts&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;contract_size&lt;/span&gt;
&lt;span class="n"&gt;vol_usd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vol_base_units&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;mark_price&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the fix: BTSE BTC ~$0.6B per day. That matches BTSE's own published volume widget on their landing page, which I should have checked first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: any time an exchange exposes a &lt;code&gt;contractSize&lt;/code&gt; field, you almost certainly have to apply it. Even if the default for BTC happens to be 1 on the venue you tested first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #3 — Kraken Futures funding rate is in dollars, not percent
&lt;/h2&gt;

&lt;p&gt;This one was the most expensive bug because it took the longest to notice.&lt;/p&gt;

&lt;p&gt;Kraken Futures' &lt;code&gt;/derivatives/api/v3/tickers&lt;/code&gt; endpoint returns a &lt;code&gt;fundingRate&lt;/code&gt; field. Every other exchange I integrate (16 of them now) returns this as a decimal: &lt;code&gt;0.0001&lt;/code&gt; means 0.01% per funding period. Annualized at 8h funding intervals, that's about 11% APY. Sane.&lt;/p&gt;

&lt;p&gt;Kraken Futures does not. Their &lt;code&gt;fundingRate&lt;/code&gt; is the &lt;strong&gt;USD payment per contract per period&lt;/strong&gt;. So a Kraken &lt;code&gt;fundingRate&lt;/code&gt; of &lt;code&gt;7.0&lt;/code&gt; doesn't mean 700% per period — it means $7 paid per contract held over the funding period.&lt;/p&gt;

&lt;p&gt;To convert to a comparable decimal rate, you divide by the mark price:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Kraken Futures returns fundingRate as USD-per-contract per period.
# Divide by mark price to get the normalized decimal rate.
&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_rate&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;mark_price&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug was visible in my dashboard for over a day before I caught it. Kraken ETH was showing &lt;strong&gt;1893% APY&lt;/strong&gt; — clearly wrong, but it sat in the "extreme rates" view alongside legitimately weird shitcoin funding rates (some alts genuinely run at hundreds of percent), so it didn't pop out. After the fix, Kraken ETH funding came back to ~7% APY, which lines up with the rest of the market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: extreme outliers in financial data are sometimes real, sometimes wrong, and almost always worth a five-minute manual sanity check before you ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line sanity check that catches all three
&lt;/h2&gt;

&lt;p&gt;Here's the trick I now run after every new exchange integration:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For BTC, sort all exchanges by 24h volume in USD. The numbers should be within one order of magnitude of each other, and no single exchange should be more than ~5x larger than the median.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Concretely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;btc_rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;btc_rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volume_24h_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;btc_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;volume_24h_usd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What healthy looks like (at the time of writing, end of March 2026):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;binance      $ 17,960,723,449
okx          $  9,153,894,221
bybit        $  7,983,221,887
gateio       $  6,510,448,775
bitget       $  4,869,514,230
hyperliquid  $  3,420,118,002
mexc         $  2,914,702,118
bingx        $  1,822,978,884
htx          $    902,541,003
btse         $    614,923,937
phemex       $    321,108,775
kucoin       $    298,992,881
bitfinex     $    132,414,498
kraken       $    101,302,440
bitmex       $     78,200,116
dydx         $     45,109,332
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The leader (Binance) is about 250x the smallest entrant (dYdX) — that's the realistic span of CEX/DEX liquidity for the dominant pair.&lt;/li&gt;
&lt;li&gt;There are no zeros, and there are no $1T entries.&lt;/li&gt;
&lt;li&gt;The shape of the curve is roughly log-linear when plotted, which is what you'd expect from the long tail of crypto venues.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any single exchange comes back with three more zeros than its neighbors, you have a unit bug. If it comes back with three fewer zeros, same thing. Both my OKX and BTSE bugs were instantly visible in this view — I just hadn't built the view until after the bugs had been live for two days.&lt;/p&gt;

&lt;p&gt;For funding rates, the equivalent sanity check is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No exchange's funding rate for a major (BTC/ETH/SOL) should annualize to more than ±100%. If one does, you have a normalization bug or the venue is in liquidation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A 1893% APY does not survive this filter. Build the filter first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in my workflow
&lt;/h2&gt;

&lt;p&gt;After fixing these three I added two things to the integration template for any new exchange:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A regression test that pins the unit semantics&lt;/strong&gt;, not just the happy path. For example, my OKX test now constructs a ticker with &lt;code&gt;volCcy24h: "138000"&lt;/code&gt; and &lt;code&gt;last: "70000"&lt;/code&gt; and asserts that &lt;code&gt;vol_usd&lt;/code&gt; equals &lt;code&gt;138_000 * 70_000&lt;/code&gt;, NOT just that the function returns a non-empty dict. If a future refactor accidentally drops the multiplication, the test fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A post-deploy cross-exchange diff check&lt;/strong&gt; that runs after every collector restart. It pulls the BTC volume across all exchanges and screams if any single one is more than 50x or less than 1/50th of the median. It's a 30-line cron job and it would have caught all three of my bugs within five minutes of deployment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both of these are straightforward, but they're easy to skip when you're heads-down adding venues. I skipped them, and it cost me two days of looking at a leaderboard with &lt;code&gt;SATS&lt;/code&gt; showing $180T in volume and thinking "huh, that's weird, I'll deal with it later."&lt;/p&gt;

&lt;p&gt;The bugs were never in the parsing code. They were in the &lt;em&gt;units of the inputs&lt;/em&gt;. The exchanges that ship the cleanest documentation and most consistent field semantics make this kind of normalization invisible. The exchanges that don't will silently corrupt your dashboard until the day you build a leaderboard.&lt;/p&gt;

&lt;p&gt;If you're aggregating data across venues, build the leaderboard first.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful and you're trading crypto perps, the dashboard I was debugging is live at &lt;a href="https://fundingfinder.foxyyy.com" rel="noopener noreferrer"&gt;https://fundingfinder.foxyyy.com&lt;/a&gt; (free tier, no signup) — it now covers 16 exchanges and ~6,200 USDT-margined perps with five-minute refresh. The fixes from this article are deployed in v0.6.10.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cryptocurrency</category>
      <category>python</category>
      <category>api</category>
      <category>debugging</category>
    </item>
    <item>
      <title>5 boring patterns I used to ship two production services on a $5 VPS in 10 days</title>
      <dc:creator>FoxyyyBusiness</dc:creator>
      <pubDate>Thu, 09 Apr 2026 22:58:05 +0000</pubDate>
      <link>https://forem.com/foxyyybusiness/5-boring-patterns-i-used-to-ship-two-production-services-on-a-5-vps-in-10-days-1kli</link>
      <guid>https://forem.com/foxyyybusiness/5-boring-patterns-i-used-to-ship-two-production-services-on-a-5-vps-in-10-days-1kli</guid>
      <description>&lt;p&gt;In the last 10 days I shipped two production web services on a single $5 Hetzner VPS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Funding Finder&lt;/strong&gt; — a 20-exchange perpetual futures funding rate aggregator. ~6,800 USDT-margined symbols, refreshed every 5 minutes. 5 systemd services, ~80 MB resident memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cronviz&lt;/strong&gt; — a stdlib-only CLI tool for unified cron + systemd timer observability. Zero dependencies. 48 unit tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both shipped with the same boring stack: &lt;strong&gt;Python 3.12 + Flask + SQLite (WAL mode) + systemd + vanilla HTML&lt;/strong&gt;. Nothing exotic. Nothing trendy. Things that have worked since 2010.&lt;/p&gt;

&lt;p&gt;This post is 5 patterns from that work. They're not the &lt;em&gt;only&lt;/em&gt; thing I used — there are about 25 more in the full collection — but they're the 5 that did the most leverage. If you're a solo dev shipping a side project on weekends and you keep getting told you need Postgres / Redis / Docker / Kubernetes / FastAPI / asyncio to be "production-ready", read these first. You probably don't.&lt;/p&gt;

&lt;p&gt;Each pattern follows the same shape: the pain it solves, the code, when &lt;em&gt;not&lt;/em&gt; to use it, and a real number from production.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The 12-line systemd unit
&lt;/h2&gt;

&lt;p&gt;You wrote a Flask app. It runs locally with &lt;code&gt;python app.py&lt;/code&gt;. You SSH into the VPS, &lt;code&gt;git pull&lt;/code&gt;, and now you need it to run &lt;em&gt;forever&lt;/em&gt;: restart on crash, restart on reboot, log to a place you can &lt;code&gt;tail -f&lt;/code&gt;, let you &lt;code&gt;systemctl restart&lt;/code&gt; it on deploy.&lt;/p&gt;

&lt;p&gt;Half the internet will tell you to use Docker. On a $5 VPS, for one process, Docker is overkill. systemd has been on every Linux box since 2015 and does exactly this in 12 lines.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/etc/systemd/system/myapp.service&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;My Flask app&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/root/myapp&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3 /root/myapp/app.py&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;
&lt;span class="py"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl daemon-reload
systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; myapp.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-f&lt;/span&gt;       &lt;span class="c"&gt;# logs&lt;/span&gt;
systemctl restart myapp.service       &lt;span class="c"&gt;# deploy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. There's nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When NOT to use it.&lt;/strong&gt; If you need rolling deploys, multiple replicas, cross-machine service discovery, or you're already on Kubernetes — skip this. For literally anything below that bar, this is the right shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production number.&lt;/strong&gt; My Funding Finder API service has been running for 10+ days, ~25 MB resident memory, 12 restarts (all from &lt;code&gt;systemctl restart&lt;/code&gt; during deploys, zero from crashes).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. SQLite in WAL mode is enough for almost any solo project
&lt;/h2&gt;

&lt;p&gt;You picked SQLite because you didn't want to run a separate database process. Then you started writing concurrently from a Flask request handler and a background collector, and SQLite locked up.&lt;/p&gt;

&lt;p&gt;The fix is two PRAGMAs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextmanager&lt;/span&gt;

&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/data/myapp.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row_factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Row&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRAGMA journal_mode=WAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRAGMA synchronous=NORMAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WAL&lt;/code&gt; means concurrent reads no longer block writes (and vice versa). &lt;code&gt;synchronous=NORMAL&lt;/code&gt; means SQLite calls &lt;code&gt;fsync()&lt;/code&gt; once per commit instead of once per page write — throughput goes from ~100 commits/sec to ~10,000 commits/sec on a typical SSD, and you only lose the last in-flight transaction in the (rare) case of a hard kernel crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When NOT to use it.&lt;/strong&gt; Networked filesystems (NFS): WAL is broken there. Use Postgres. Multi-machine deployments: same answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production number.&lt;/strong&gt; My collector inserts ~6,800 rows in one transaction every 5 minutes. With WAL: ~80 ms per batch, API server reads never block. Without WAL: random 500s during commit windows.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Per-IP rate limiting in 25 lines, no Redis
&lt;/h2&gt;

&lt;p&gt;You opened your free-tier API to the public. Within hours, one IP starts hammering it 50 req/sec. You need to throttle without (a) blocking entirely, (b) running Redis, (c) installing a "professional" rate limiting library that needs a year of config.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Lock&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abort&lt;/span&gt;

&lt;span class="n"&gt;_hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;_WINDOW_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="n"&gt;_MAX_PER_WINDOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;   &lt;span class="c1"&gt;# 60 req/min/IP
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rate_limit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remote_addr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_WINDOW_SECONDS&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;dq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popleft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;_MAX_PER_WINDOW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;_WINDOW_SECONDS&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate limit, retry in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/expensive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;api_expensive&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;rate_limit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each IP gets a deque of recent timestamps. Drop expired ones, count, abort if over the limit, otherwise append. ~25 lines, one global dict, one lock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When NOT to use it.&lt;/strong&gt; Multi-process deployments (gunicorn &lt;code&gt;--workers=4&lt;/code&gt;) where each worker has its own dict and the limit becomes per-worker. The fix is gunicorn &lt;code&gt;--workers=1 --threads=8&lt;/code&gt; (fine for I/O-bound services), or move to Redis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production number.&lt;/strong&gt; My free tier is 60 req/min/IP, paid tiers are 600 and 3000 req/min. 10 days, 10k+ unique IPs, ~5 MB total memory cost across all the tracked deques. Zero issues.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. ThreadPoolExecutor for fan-out HTTP fetching, when async is overkill
&lt;/h2&gt;

&lt;p&gt;You need to fetch funding rates for 285 OKX perpetual contracts. The exchange rate-limits public endpoints to ~10 req/s. Your options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sequential.&lt;/strong&gt; 285 × 100 ms = 28.5 seconds. Too slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aiohttp&lt;/code&gt; + &lt;code&gt;asyncio.gather&lt;/code&gt;.&lt;/strong&gt; Fast, but you're now rewriting the codebase in &lt;code&gt;async def&lt;/code&gt;, your tests need &lt;code&gt;pytest-asyncio&lt;/code&gt;, your stack traces are 80% framework noise, and you're debugging "RuntimeError: This event loop is already running" in REPL sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;concurrent.futures.ThreadPoolExecutor&lt;/code&gt;.&lt;/strong&gt; 8 worker threads, 8 seconds end-to-end, zero new dependencies, your code stays synchronous.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;as_completed&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;myapp/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inst_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.okx.com/api/v5/public/funding-rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;instId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inst_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inst_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch_one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;inst&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;inst&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inst_ids&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fut&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;as_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick worker count just below &lt;code&gt;(rate_limit_per_sec × p95_latency_seconds) × 2&lt;/code&gt;. For OKX (10 req/s, 150 ms median): 8 workers, comfortably under the limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When NOT to use it.&lt;/strong&gt; CPU-bound work (the GIL doesn't help — use ProcessPoolExecutor or NumPy). Or millions of concurrent connections (~1 MB stack per thread; 10k threads = 10 GB RAM — use async). For "fan out 100-500 HTTP calls under a rate limit", threads are the boring, working answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production number.&lt;/strong&gt; My OKX fetcher: 285 instruments, 8 worker threads, ~8 seconds end-to-end, &amp;lt; 0.1% failure rate over 10 days. The async version would shave ~3 seconds off and require rewriting half my codebase. Not worth it.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Telegram bot as monitoring transport — €0/month
&lt;/h2&gt;

&lt;p&gt;Your service crashes at 3 AM. PagerDuty exists for this and costs €19/user/month. Telegram exists for this and costs €0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup (90 seconds):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Telegram, search &lt;code&gt;@BotFather&lt;/code&gt;, send &lt;code&gt;/newbot&lt;/code&gt;. Get a token.&lt;/li&gt;
&lt;li&gt;Send your new bot a message (you must message it first, before it can message you).&lt;/li&gt;
&lt;li&gt;Visit &lt;code&gt;https://api.telegram.org/bot&amp;lt;TOKEN&amp;gt;/getUpdates&lt;/code&gt; and find &lt;code&gt;"chat":{"id":12345678}&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CHAT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TELEGRAM_CHAT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;CHAT_ID&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.telegram.org/bot&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sendMessage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CHAT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parse_mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it from anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;disk_usage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;⚠️ disk at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;disk_usage&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; on `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;`&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;collector_age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;❌ collector stale: last fetch &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;collector_age&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s ago&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✅ deploy of `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;commit_sha&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;` complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your phone buzzes within 2 seconds. No SaaS account, no SDK, no webhook UI, no escalation policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When NOT to use it.&lt;/strong&gt; On-call rotation. Acknowledgment + escalation. Compliance audit trails. &amp;gt; 30 messages/sec sustained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production number.&lt;/strong&gt; I've used this pattern in every service I've shipped for 3 years. Zero missed alerts. Zero subscription cost. Telegram has not become a product they're trying to monetize — the Bot API has been stable since 2015.&lt;/p&gt;




&lt;h2&gt;
  
  
  The take-away
&lt;/h2&gt;

&lt;p&gt;Shipping a paid web service on a $5 VPS doesn't require 47 config files, a microservices architecture, or a year of devops yak-shaving. It requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One systemd unit per service&lt;/li&gt;
&lt;li&gt;SQLite in WAL mode&lt;/li&gt;
&lt;li&gt;A 25-line rate limiter&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; instead of asyncio&lt;/li&gt;
&lt;li&gt;A free Telegram bot for alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total dependencies introduced by the 5 patterns above: &lt;strong&gt;&lt;code&gt;requests&lt;/code&gt;&lt;/strong&gt; (and even that is optional — you could use &lt;code&gt;urllib.request&lt;/code&gt; from stdlib). That's it.&lt;/p&gt;

&lt;p&gt;If you're still in "I should learn Kubernetes before I ship anything" mode, please consider that this entire stack costs €5/month, fits on a $5 VPS, has been running two production services for 10 days with zero downtime, and is fundamentally easier to reason about than any "modern" alternative. Use what works. The boring stack works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this comes from
&lt;/h2&gt;

&lt;p&gt;These 5 patterns are extracted from a longer collection I'm putting together: &lt;strong&gt;30 Boring Patterns for Solo Devs Who Ship&lt;/strong&gt;. The other 25 cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Theme 2&lt;/strong&gt; — SQLite at solo-dev scale (migrations, backups, single-writer pattern, cursor pagination)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme 3&lt;/strong&gt; — HTTP and Flask (API key auth with revocation, CSV exports, OpenAPI by hand, vanilla HTML/SVG dashboards)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme 4&lt;/strong&gt; — More external API patterns (mocked-HTTP testing, the structural sanity check on aggregate output, per-source field normalization)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme 5&lt;/strong&gt; — Operations and observability (health endpoints, /metrics in 15 lines, &lt;code&gt;make&lt;/code&gt; as your only deploy tool)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme 6&lt;/strong&gt; — Going from free to paid (per-tier rate limiting, Lemon Squeezy vs Stripe, pricing a niche dev tool)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full pack (standalone HTML + companion code zip) is €19, one-time, no subscription. Live now at &lt;a href="https://foxyyy.com/boring-patterns" rel="noopener noreferrer"&gt;https://foxyyy.com/boring-patterns&lt;/a&gt; — instant download after checkout via Stripe. The 5 patterns above are the full content of those 5 patterns — no teaser cuts. If they're useful, the other 25 are too. If they're not, you've still got 5 production-tested patterns for free. Patterns are CC BY-SA 4.0, code is MIT, 14-day no-questions refund.&lt;/p&gt;

&lt;p&gt;Comments / corrections / "you're wrong about X" replies very welcome.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>sqlite</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>30 days of solo dev shipping: 9 projects, 1 VPS, no Docker — what I actually learned</title>
      <dc:creator>FoxyyyBusiness</dc:creator>
      <pubDate>Thu, 09 Apr 2026 22:53:44 +0000</pubDate>
      <link>https://forem.com/foxyyybusiness/30-days-of-solo-dev-shipping-9-projects-1-vps-no-docker-what-i-actually-learned-47a3</link>
      <guid>https://forem.com/foxyyybusiness/30-days-of-solo-dev-shipping-9-projects-1-vps-no-docker-what-i-actually-learned-47a3</guid>
      <description>&lt;p&gt;I'm a solo dev. Over the last 30 days I shipped 9 distinct projects on a single $5 Hetzner VPS, all running concurrently, all publicly accessible right now. Total Docker containers: zero. Total Postgres processes: zero. Total cumulative downtime: zero.&lt;/p&gt;

&lt;p&gt;This is a retrospective. Not a Show HN, not a launch announcement, not a "look at my projects" gallery. I want to write down what I actually learned doing this — the bets that paid off, the bets that humbled me, and the framework I converged on by accident around day 12.&lt;/p&gt;

&lt;p&gt;If you're a solo dev who keeps reading "ultimate stack for indie hackers" posts and thinking "but I just want to ship something already", this is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 9 projects in one paragraph
&lt;/h2&gt;

&lt;p&gt;A cross-exchange perpetual futures funding rate scanner across 20 venues (data SaaS, the primary product). A stdlib-only CLI for unified cron and systemd timer observability (an OSS dev tool). An info-product book of 30 production-tested patterns for shipping side projects on a $5 VPS. An autonomous bot that posts hourly funding rate signals to Telegram and a public web feed. An auto-generated daily research blog that templates a structured Markdown post from live data. A directory of 10 trader calculators, each at its own URL for long-tail SEO. A curated directory of infra resources for solo devs. An uptime tracker for the 20 exchanges, with 24h status pages per venue. A packaged historical funding-rate dataset (2.58M rows, daily rebuild, CC BY 4.0). Nine distinct domains, nine distinct customer types, all running on the same Flask + SQLite + systemd stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The framework that emerged by accident
&lt;/h2&gt;

&lt;p&gt;I started with one project (the cross-exchange scanner). I assumed I'd spend the full 30 days polishing it. Within ~10 days the scanner had everything it needed technically, and I was about to start working on features that the scanner &lt;em&gt;didn't need&lt;/em&gt; because I had nothing else to do. That's the moment I realized the gating items were no longer technical — they were external. Distribution accounts (Reddit, HN, Twitter), payment processor (Lemon Squeezy), domain name. None of these are problems I can code my way out of.&lt;/p&gt;

&lt;p&gt;So I parked the scanner and started a second project in a completely different domain. Then a third. By day 18 the framework had crystallized into three rules:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1 — Park when blocked, not when bored.&lt;/strong&gt; A "real blocker" is something I cannot resolve alone (a credential, a decision from someone else, a payment, a validation). Not "I want to add another feature". Not "I could improve the test coverage". If I can still write code, prose, tests, or design — that's not a blocker, that's continuation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2 — Always have ≥2 projects in flight.&lt;/strong&gt; When I park a project, I start (or resume) something else immediately. The work queue is never empty. This sounds inefficient but it's actually the opposite: it forces me to never be in the "I'm waiting for X" passive state. There's always a concrete next action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 3 — When the projects start to look alike, the rule kicks in differently.&lt;/strong&gt; When I had 3 projects and they were all "Flask data API for crypto", I stopped and asked: am I just rebuilding the same thing in different domains? The answer was yes. So I started forcing the new projects to be &lt;em&gt;structurally&lt;/em&gt; different from existing ones — different audience, different distribution model, different format. The 9 projects ended up covering 9 distinct shapes (data SaaS, OSS CLI, info product, autonomous notifications, auto-generated content, calculators, curated directory, status pages, packaged dataset). Each one would teach me something the others couldn't.&lt;/p&gt;

&lt;p&gt;I didn't plan this framework. It emerged because the alternative — sitting on one project waiting for credentials — felt obviously wasteful. Once it was named, it became operational.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring stack paid off, again
&lt;/h2&gt;

&lt;p&gt;Every project ships with Python 3.12 + Flask + SQLite (WAL mode) + systemd + vanilla HTML. No Docker, no Postgres, no Redis, no Kafka, no asyncio, no frontend framework, no microservices, no API gateway, no serverless functions.&lt;/p&gt;

&lt;p&gt;The reason this works is mostly negative: every "modern" thing I avoided has a real cost in setup time, debugging surface, and ongoing maintenance, and the value those things would provide doesn't matter at solo-dev scale. SQLite in WAL mode handles 50,000 commits per second on a $5 VPS with NVMe — every project I built combined uses maybe 200 writes per minute. That's 0.4% of capacity. Postgres would make zero perceptible difference and would add a process to manage, a separate auth surface, a backup story, and a network round-trip for every query.&lt;/p&gt;

&lt;p&gt;systemd does the job of Docker for any single-host service. Twelve lines per unit file gets you auto-restart on crash, auto-start on reboot, structured logging via &lt;code&gt;journalctl&lt;/code&gt;, and &lt;code&gt;systemctl restart&lt;/code&gt; deploys in 3 seconds. I use it for every service I ship and I've never had a moment where I missed Docker.&lt;/p&gt;

&lt;p&gt;The most surprising thing about the boring stack is how &lt;em&gt;small&lt;/em&gt; it feels. The whole 9-project ecosystem fits in your head. I can hold the entire surface area mentally: 5 systemd services, 1 SQLite file, ~25 HTML pages, ~30 API endpoints, 3 autonomous timers, 80 MB of resident memory total. There are no hidden processes, no opaque containers, no black-box managed services. If something is wrong, I know where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four bugs that humbled me
&lt;/h2&gt;

&lt;p&gt;While integrating the 20 exchanges, I introduced four silent bugs in my own normalization code that 100% test coverage didn't catch. Each was a unit conversion error and each was off by 5+ orders of magnitude:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OKX &lt;code&gt;volCcy24h&lt;/code&gt; is in base coin units, not USDT.&lt;/strong&gt; I was treating it as USDT. OKX BTC volume came back as $136k against Binance's $16.5B. I'd been staring at this in the dashboard for two days without noticing because BTC was already at the top of the ranking — &lt;em&gt;the order was right, the magnitude was off by 5 orders&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;BTSE &lt;code&gt;volume&lt;/code&gt; is contract count, not base coin units.&lt;/strong&gt; Without the contract size multiplier, my BTC volume calc was 5 orders too high. The leaderboard showed BTSE BTC at $60T (&amp;gt;$60 trillion). Anyone who read the dashboard would have noticed instantly. I didn't, because I had no aggregate view.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kraken Futures &lt;code&gt;fundingRate&lt;/code&gt; is USD-per-contract per period, not decimal.&lt;/strong&gt; Their &lt;code&gt;fundingRate&lt;/code&gt; of &lt;code&gt;7.0&lt;/code&gt; means "$7 per contract per period", not "700% per period". I was treating it as decimal and showing Kraken ETH at 1893% APY for two days.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;BitMEX uses XBT internally, not BTC.&lt;/strong&gt; I'd written XBT→BTC normalization for Kraken and KuCoin (which also use XBT) months earlier, but forgot to add it for BitMEX when I integrated it. BitMEX BTC silently disappeared from cross-venue BTC views for the entire time it was in production. Caught only when I added the 17th venue and noticed the leaderboard had 16 entries instead of 17.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What unifies these four bugs is that &lt;strong&gt;none of them were in the parsing code&lt;/strong&gt;. The parsing code was correct in every case. The bugs were in the &lt;em&gt;assumed semantics of the input&lt;/em&gt;. My unit tests were green because they pinned the wrong invariants on hand-written JSON fixtures that I had also gotten wrong.&lt;/p&gt;

&lt;p&gt;The thing that finally caught all four was a single 30-line function I started running after every collection cycle: sort BTC volume across all sources, look for any source where the value is more than 50× the median or less than 1/50× the median. If you have an outlier of that magnitude, it's almost always a unit bug. I now run this check in CI for every new exchange integration and it has caught zero false positives and four real bugs.&lt;/p&gt;

&lt;p&gt;I wrote this up under the working name "structural sanity check on aggregate output" but I'm not sure that's what it's called. Property-based testing is the closest formal cousin but doesn't quite fit. If anyone has a better name, I'd love it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start the launch material on day one, not day twenty.&lt;/strong&gt; I waited until I had products to launch before writing the launch drafts. By the time I wrote them, I had four projects without any distribution material at all — orphans. If I were doing this again, I'd write the Show HN draft for a project the same day I started building it, even before v0.1. The exercise of writing the draft forces clarity about what the project is &lt;em&gt;for&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the placeholder-replacement script earlier.&lt;/strong&gt; I have 70+ placeholders scattered across drafts and static pages (&lt;code&gt;clementslowik&lt;/code&gt;, &lt;code&gt;clementslowik&lt;/code&gt;, &lt;code&gt;github.com/clementslowik/funding-collector&lt;/code&gt;, &lt;code&gt;fundingfinder.foxyyy.com&lt;/code&gt;) that all need to be replaced when credentials arrive. I wrote the replacement script in week 4. If I'd written it on day 1, every new draft would have used the placeholder syntax from the start, and the launch-day patching would have been trivial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Take the credential blockers seriously earlier.&lt;/strong&gt; I assumed that the credentials (Reddit account, HN karma, GitHub PAT, Lemon Squeezy account) would arrive "soon" and I could focus on the build. They didn't arrive in week 1, then they didn't arrive in week 2, then I started genuinely understanding that the entire monetization pipeline was gated on items I couldn't resolve myself. If I were doing this again, I'd treat credential acquisition as the very first task, not the last.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write the meta-launch piece earlier.&lt;/strong&gt; The single Show HN that points at /shipped (the canonical "I shipped 9 projects on a $5 VPS in 30 days" page) is going to be the most important post of the entire 30 days, because it serves as proof for all 9 projects simultaneously. I wrote it on day 30. It should have been drafted on day 5, with the project list updated as new ones shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wouldn't change
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;8-projects-in-30-days pace&lt;/strong&gt;. I expected to feel scattered and unfocused. Instead I feel like I have a much wider sense of what each kind of product feels like to build, which is exactly the kind of generalist intuition you don't get from focusing on one thing. The opportunity cost is real (none of the 8 is maximally polished), but the &lt;em&gt;learning rate&lt;/em&gt; is 8x higher than it would have been on one project.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;boring stack discipline&lt;/strong&gt;. Not once did I think "if only I had Docker" or "this would be easier with Postgres". Every time I was tempted to add a new tool, the question "what specific problem does this solve that I have right now" produced an honest "none". The boring stack saved me weeks of decision fatigue.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;work_queue framework&lt;/strong&gt;. Having a hard rule of "≥2 projects in flight, park when actually blocked, start something different when bored" turned out to be the productivity hack I needed. Not the sexy productivity hack, the boring one — the one that says you don't need a Pomodoro timer, you need a clear next action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Everything I built is publicly accessible right now. If you want to verify any of the claims above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;/shipped&lt;/strong&gt; — the canonical list of 9 projects with public URLs and metrics: &lt;a href="https://foxyyy.com/shipped" rel="noopener noreferrer"&gt;https://foxyyy.com/shipped&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;/now&lt;/strong&gt; — the cross-exchange funding rate dashboard: &lt;a href="https://foxyyy.com/now" rel="noopener noreferrer"&gt;https://foxyyy.com/now&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;/research/2026-04-09&lt;/strong&gt; — today's auto-generated research post&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;/status&lt;/strong&gt; — uptime tracker for the 20 exchanges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;/boring-patterns&lt;/strong&gt; — the in-progress patterns book (5 free, 12 more drafted)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source code for the OSS data collector: pip-installable from &lt;code&gt;https://foxyyy.com/downloads/funding-collector-0.4.3.tar.gz&lt;/code&gt; (will move to GitHub when credentials arrive).&lt;/p&gt;

&lt;p&gt;If you found this useful, the easiest way to support is to bookmark &lt;code&gt;/now&lt;/code&gt; and check it once a day. There's a launch waitlist on every project page if you want the one-time email when the paid tiers go live — no spam, no follow-up sequence, single email.&lt;/p&gt;

&lt;p&gt;Comments / corrections / "you're wrong about X" replies are very welcome. The "structural sanity check" pattern naming question in particular is a real ask — if you have a better name for it, I'm reading every reply.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>python</category>
      <category>sysadmin</category>
    </item>
  </channel>
</rss>
