<?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>DEV Community: Sancho Brojinga</title>
    <description>The latest articles on DEV Community by Sancho Brojinga (@brojinga).</description>
    <link>https://dev.to/brojinga</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4181448%2F173c265c-339b-49d4-8eb1-67c1c7d0855d.png</url>
      <title>DEV Community: Sancho Brojinga</title>
      <link>https://dev.to/brojinga</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brojinga"/>
    <language>en</language>
    <item>
      <title>What I Learned Building a Funding Rate Comparison Tool (and Where Exchange APIs Get Weird)</title>
      <dc:creator>Sancho Brojinga</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:35:02 +0000</pubDate>
      <link>https://dev.to/brojinga/what-i-learned-building-a-funding-rate-comparison-tool-and-where-exchange-apis-get-weird-j13</link>
      <guid>https://dev.to/brojinga/what-i-learned-building-a-funding-rate-comparison-tool-and-where-exchange-apis-get-weird-j13</guid>
      <description>&lt;p&gt;I built a small tool that compares perpetual funding rates across five exchanges. The project was straightforward until it wasn't. Here's what I learned about crypto exchange APIs — the parts nobody documents.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Not all exchanges return data in the same order&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Binance's funding history endpoint returns entries oldest-first. Bybit's returns newest-first. Assume one ordering and half your data is wrong for weeks without noticing.&lt;/p&gt;

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

&lt;p&gt;python&lt;br&gt;
entries.sort(key=lambda e: e[0], reverse=True)&lt;br&gt;
One line. But finding it took a diagnostic run that printed the raw array and its timestamps to see the pattern.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Geo-blocking is real and deliberate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Binance and Bybit both block requests from cloud infrastructure. GitHub Actions (Azure IPs), Cloudflare Workers, AWS — all blocked. The response codes vary: Binance returns HTTP 451 with a "Service unavailable from a restricted location" message; Bybit returns HTTP 403 through their CloudFront distribution with "The Amazon CloudFront distribution is configured to block access from your country."&lt;/p&gt;

&lt;p&gt;This isn't a bug. It's an anti-scraping policy. The workaround is to use exchanges that don't geo-block cloud IPs — OKX, MEXC, Hyperliquid, Gate.io, Bitget all work fine.&lt;/p&gt;

&lt;p&gt;I tried a Cloudflare Worker proxy to route around the block. It worked for Bybit from my laptop but failed from the GitHub Actions runner. Eventually I dropped both exchanges and added five that don't block us.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Funding intervals vary widely&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most exchanges use 8-hour funding. Some use 4-hour. Hyperliquid uses 1-hour. If you annualize assuming 8 hours across the board, you're wrong by a factor of 8 on Hyperliquid.&lt;/p&gt;

&lt;p&gt;The correct formula uses the venue's own interval:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
periods_per_day = 24 / interval_hours&lt;br&gt;
apr = ((1 + rate) ** (periods_per_day * 365) - 1) * 100&lt;br&gt;
Each venue's funding interval comes from the settlement timestamps — compute the median gap between consecutive entries and you have it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Always verify response shapes before writing the parser&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I nearly shipped a version that used a single per-symbol rate and four fee assumptions to compute "venue-specific" funding. It would have told users "Binance is the best venue" when the conclusion came from Binance's assumed fee, not its actual rate.&lt;/p&gt;

&lt;p&gt;The fix: fetch real per-venue data. Which meant verifying every endpoint's response shape before writing the parser. It sounds tedious. It saves you from shipping misleading numbers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Static beats live for this use case&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Funding rates only change at settlement. A snapshot refreshed every 8 hours is maximally fresh for the entire window. Fetching live at page load burns API quota, adds latency, and introduces a failure point for zero benefit.&lt;/p&gt;

&lt;p&gt;The architecture:&lt;/p&gt;

&lt;p&gt;GitHub Actions cron runs the fetcher every 8 hours&lt;/p&gt;

&lt;p&gt;Fetcher writes to a JSON file, commits it&lt;/p&gt;

&lt;p&gt;Cloudflare Pages auto-rebuilds on the commit&lt;/p&gt;

&lt;p&gt;Visitor loads a pre-rendered static page in under 50ms&lt;/p&gt;

&lt;p&gt;The tool&lt;/p&gt;

&lt;p&gt;&lt;a href="https://feescope.io" rel="noopener noreferrer"&gt;https://feescope.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: github.com/sanchopansa1987/feescope&lt;/p&gt;

&lt;p&gt;If you're building anything on exchange APIs, the surprises are always in the response shapes and the geo-blocks. Everything else is straightforward.&lt;/p&gt;

</description>
      <category>cryptocurrency</category>
      <category>api</category>
      <category>webdev</category>
      <category>python</category>
    </item>
  </channel>
</rss>
