<?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: Koray KÖYLÜ</title>
    <description>The latest articles on DEV Community by Koray KÖYLÜ (@koraykoylu).</description>
    <link>https://dev.to/koraykoylu</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%2F3956907%2F5240262c-b071-4103-98d3-9257c11f4c4f.png</url>
      <title>DEV Community: Koray KÖYLÜ</title>
      <link>https://dev.to/koraykoylu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/koraykoylu"/>
    <language>en</language>
    <item>
      <title>A payment QR code is just twelve lines of text</title>
      <dc:creator>Koray KÖYLÜ</dc:creator>
      <pubDate>Wed, 12 Aug 2026 04:56:12 +0000</pubDate>
      <link>https://dev.to/koraykoylu/a-payment-qr-code-is-just-twelve-lines-of-text-1bg4</link>
      <guid>https://dev.to/koraykoylu/a-payment-qr-code-is-just-twelve-lines-of-text-1bg4</guid>
      <description>&lt;p&gt;If you have paid an invoice in Europe recently, you have probably pointed your banking app at a small square and watched it fill in the payee, the IBAN and the amount by itself. I run &lt;a href="https://ibanchecker.cash" rel="noopener noreferrer"&gt;ibanchecker.cash&lt;/a&gt; and I spent the last few weeks on both sides of those squares: generating them and reading them back. The format turned out to be far simpler than most people expect, and a couple of its rules surprised me even after years around IBANs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually in there
&lt;/h2&gt;

&lt;p&gt;The standard is EPC069-12, published by the European Payments Council. German banks market the result as a GiroCode. There is no binary framing and no JSON. The payload is plain text, twelve elements separated by line breaks. This is the exact content of the code on the cover image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BCD
002
1
SCT
COBADEFF
Webajans Bilisim Ltd
DE89370400440532013000
EUR149.00


Invoice 2026-014
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line&lt;/th&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;In this code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Service tag&lt;/td&gt;
&lt;td&gt;always &lt;code&gt;BCD&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Version&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;002&lt;/code&gt;, which makes the BIC optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Character set&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1&lt;/code&gt; means UTF-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Identification&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SCT&lt;/code&gt;, a SEPA Credit Transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;BIC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COBADEFF&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Beneficiary name&lt;/td&gt;
&lt;td&gt;who gets paid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;IBAN&lt;/td&gt;
&lt;td&gt;the account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Amount&lt;/td&gt;
&lt;td&gt;currency glued to the value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;empty here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Structured reference&lt;/td&gt;
&lt;td&gt;empty here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Unstructured reference&lt;/td&gt;
&lt;td&gt;free text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Beneficiary to originator&lt;/td&gt;
&lt;td&gt;may be absent entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few rules worth knowing before you implement any of this. The whole payload must fit in 331 bytes. The name is capped at 70 characters and the reference at 140. The amount is euro only, from 0.01 to 999,999,999.99; the standard simply has no concept of another currency. Error correction must be level M. And there is no signature anywhere: nothing in the code proves who produced it, which is worth remembering the next time a QR sticker appears on a parking meter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field we got wrong
&lt;/h2&gt;

&lt;p&gt;Line 6 is the beneficiary name. The account holder. The person or company the money goes to.&lt;/p&gt;

&lt;p&gt;The first version of our generator wrote the detected bank name there. Scan such a code and your banking app politely suggests paying "Commerzbank" instead of the person behind the account. It ran like that for about three weeks before we caught it. If you take one thing from this article: line 6 is the payee, never the bank. It is an easy mistake to make because the bank name is usually the string you have just resolved from the IBAN and it is sitting right there in a variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuilding the generator
&lt;/h2&gt;

&lt;p&gt;The old tool had a Generate button. The new one updates the code live as you type, which sounds cosmetic but changes how people use it: you see the code grow as the reference gets longer, and you see the byte counter move toward the 331 limit.&lt;/p&gt;

&lt;p&gt;Exports are SVG, high resolution PNG and a vector PDF. The PDF was the fun part: a QR code is nothing but rectangles, so instead of pulling in a PDF library we walk the SVG path data and emit the rectangles into a hand-written PDF. Zero dependencies. Dropping a logo into the center automatically raises error correction to H.&lt;/p&gt;

&lt;p&gt;One design decision I want to defend explicitly: if you enter an amount in a currency other than the euro, the tool still generates the code but tells you plainly that banking apps outside SEPA may not read it. The alternative, silently producing something that looks official, felt wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading codes back
&lt;/h2&gt;

&lt;p&gt;The newer tool is the reverse: &lt;a href="https://ibanchecker.cash/tools/decode-payment-qr" rel="noopener noreferrer"&gt;drop in a picture of a payment QR&lt;/a&gt; and it lists all twelve elements with their byte counts, measured against the limits the standard sets. Decoding happens entirely in the browser with a dynamically imported jsQR; the image is never uploaded anywhere. The bytes are re-decoded with whatever character set the payload declares, so an ISO 8859 umlaut survives.&lt;/p&gt;

&lt;p&gt;The IBAN inside then runs through the same engine as the rest of the site: country, length, national structure, the ISO 13616 check digits, the national account check digit where a country defines one (25 countries so far; for Germany that means the per-bank Prüfziffer methods, for the UK the VocaLink modulus check) and a directory lookup for the bank and its BIC.&lt;/p&gt;

&lt;p&gt;Two rules were fixed before the first line of code. The image never leaves the browser. And decoded text is never rendered as a clickable link, because QR phishing works precisely by making that tap convenient.&lt;/p&gt;

&lt;p&gt;One honest limitation: Swiss QR-bills are recognized but not parsed. They follow a different standard from SIX, not the EPC one, and mapping their fields onto the EPC layout would be guessing dressed up as decoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the cover image
&lt;/h2&gt;

&lt;p&gt;The QR on the cover is real. I generated it with the first tool and verified it by feeding the finished image back into the second one. The IBAN in it is the well-known documentation example, DE89 3704 0044 0532 0130 00, not a live account.&lt;/p&gt;

&lt;p&gt;Both tools are free and need no signup: the &lt;a href="https://ibanchecker.cash/tools/iban-qr-code" rel="noopener noreferrer"&gt;generator&lt;/a&gt; and the &lt;a href="https://ibanchecker.cash/tools/decode-payment-qr" rel="noopener noreferrer"&gt;decoder&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>payments</category>
      <category>fintech</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>We built a free ipify alternative - here's what we learned</title>
      <dc:creator>Koray KÖYLÜ</dc:creator>
      <pubDate>Fri, 12 Jun 2026 20:23:26 +0000</pubDate>
      <link>https://dev.to/koraykoylu/we-built-a-free-ipify-alternative-heres-what-we-learned-39eb</link>
      <guid>https://dev.to/koraykoylu/we-built-a-free-ipify-alternative-heres-what-we-learned-39eb</guid>
      <description>&lt;p&gt;If you've ever needed your public IP in a script, you've probably typed this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl api.ipify.org
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ipify is one of those quiet pieces of internet infrastructure. Launched in 2014, it answers one question — &lt;em&gt;"what is my public IP?"&lt;/em&gt; — so simply that it ended up embedded in millions of scripts and apps, serving billions of requests a month. Respect.&lt;/p&gt;

&lt;p&gt;But a while ago I noticed something while browsing its &lt;a href="https://github.com/rdegges/ipify-api" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;: the last code change was &lt;strong&gt;January 2018&lt;/strong&gt;. Community pull requests — CORS headers, Docker support, even one-line README fixes — have been sitting unanswered for years. The hosted service still runs (it's commercially operated by WhoisXML API), but the open-source project that made developers trust it is effectively frozen. And geolocation was never part of the free deal — it's a separate paid product.&lt;/p&gt;

&lt;p&gt;That gap looked like an invitation. We run &lt;a href="https://whatsmy.fyi" rel="noopener noreferrer"&gt;whatsmy.fyi&lt;/a&gt;, an IP and network info tool, and we decided to build the thing we wished existed. Here's what we shipped and — more usefully — what we learned along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;p&gt;A keyless endpoint that's deliberately compatible with ipify's response format:&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;# Plain text&lt;/span&gt;
curl https://whatsmy.fyi/ip
&lt;span class="c"&gt;# → 203.0.113.42&lt;/span&gt;

&lt;span class="c"&gt;# JSON — identical shape to api.ipify.org&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://whatsmy.fyi/ip?format=json"&lt;/span&gt;
&lt;span class="c"&gt;# → {"ip":"203.0.113.42"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swapping it into existing code is a one-URL change. No key, no signup, CORS enabled, IPv4 + IPv6, nothing logged.&lt;/p&gt;

&lt;p&gt;When you need more than the bare IP, a free API key (10,000 requests/day) returns city, country, coordinates, timezone, ASN, and ISP — the data ipify sells separately. There's also a &lt;a href="https://github.com/koraykoylu/whatsmyfyi-sdk" rel="noopener noreferrer"&gt;TypeScript SDK&lt;/a&gt; with a keyless mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WhatsMyFyi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@whatsmyfyi/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WhatsMyFyi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;           &lt;span class="c1"&gt;// no key needed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;address&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;// "203.0.113.42"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lesson 1: friction decides everything in this category
&lt;/h2&gt;

&lt;p&gt;Our API originally required a key for everything. Reasonable, right? Usage tracking, rate limits, dashboards.&lt;/p&gt;

&lt;p&gt;Wrong — at least for this use case. The entire reason &lt;code&gt;curl api.ipify.org&lt;/code&gt; won the internet is that it works &lt;em&gt;before you've decided to care&lt;/em&gt;. A developer comparing IP APIs closes the tab the moment they see "sign up first." We realized the keyless endpoint wasn't a nice-to-have; it was the price of admission. The authenticated API now earns its keep as the &lt;em&gt;upgrade path&lt;/em&gt;, not the front door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: the edge changed the economics
&lt;/h2&gt;

&lt;p&gt;ipify's era required choices: serving geolocation means licensing a GeoIP database, updating it, and eating lookup latency. Charging for geo made sense in 2014.&lt;/p&gt;

&lt;p&gt;Running on Cloudflare Workers changed that math for us. Every request already arrives with geolocation, ASN, and timezone attached (the &lt;code&gt;request.cf&lt;/code&gt; object) — data from the network itself, with no external database call. Serving "rich" IP data now costs us roughly the same as serving the bare IP. That's not cleverness on our part; it's a structural shift that makes the old free-IP/paid-geo split feel dated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 3: honesty is a feature you can ship
&lt;/h2&gt;

&lt;p&gt;We published a &lt;a href="https://whatsmy.fyi/compare" rel="noopener noreferrer"&gt;comparison page&lt;/a&gt; covering ipify, icanhazip, ip-api.com, ipapi.co, and ipinfo — and we verified every claim by calling each API ourselves (ip-api.com really does reject HTTPS on the free tier; ipapi.co really did 429 our first request). The page also lists the places where competitors beat us, and recommends icanhazip for shell scripts and ipinfo for enterprise needs, because that's the truth.&lt;/p&gt;

&lt;p&gt;This felt risky to write. But developers can smell a rigged comparison table from a mile away; an honest one is rare enough to be memorable — and it gives every claim on the page a "verify it yourself" quality that no marketing copy can match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 4: maintenance is the moat
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable part: nothing we built is technically hard. A keyless IP endpoint is a weekend project. What ipify's story actually demonstrates is that the hard part is &lt;em&gt;still being there&lt;/em&gt; — answering issues, merging PRs, shipping releases — years after the launch excitement fades.&lt;/p&gt;

&lt;p&gt;So we wrote the only durable differentiator we could into our &lt;a href="https://github.com/koraykoylu/whatsmyfyi-sdk/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;: every issue and PR gets a human response within a few days. If we ever stop honoring that, someone should build the whatsmy.fyi alternative — and they'd be right to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keyless endpoint: &lt;code&gt;curl https://whatsmy.fyi/ip&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://whatsmy.fyi/docs" rel="noopener noreferrer"&gt;API docs&lt;/a&gt; · &lt;a href="https://whatsmy.fyi/openapi.json" rel="noopener noreferrer"&gt;OpenAPI spec&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/koraykoylu/whatsmyfyi-sdk" rel="noopener noreferrer"&gt;SDK on GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/@whatsmyfyi/sdk" rel="noopener noreferrer"&gt;npm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://whatsmy.fyi/compare" rel="noopener noreferrer"&gt;The full comparison&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions, criticism, and "actually you got X wrong" comments are genuinely welcome — that's how the comparison page stays honest.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>api</category>
      <category>opensource</category>
    </item>
    <item>
      <title>IP geolocation with zero external APIs, the Cloudflare Workers cf object</title>
      <dc:creator>Koray KÖYLÜ</dc:creator>
      <pubDate>Wed, 10 Jun 2026 12:49:39 +0000</pubDate>
      <link>https://dev.to/koraykoylu/ip-geolocation-with-zero-external-apis-the-cloudflare-workers-cf-object-16e</link>
      <guid>https://dev.to/koraykoylu/ip-geolocation-with-zero-external-apis-the-cloudflare-workers-cf-object-16e</guid>
      <description>&lt;p&gt;When I built &lt;a href="https://whatsmy.fyi" rel="noopener noreferrer"&gt;whatsmy.fyi&lt;/a&gt;, I assumed I'd need a geolocation provider: MaxMind, ipinfo, ip-api, pick your poison. They all mean the same thing: an external dependency, an API key, a quota, added latency, and someone else's server seeing your users' IPs.&lt;/p&gt;

&lt;p&gt;Then I found out Cloudflare Workers makes the whole category unnecessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;cf&lt;/code&gt; object
&lt;/h2&gt;

&lt;p&gt;Every request that hits a Cloudflare Worker carries a &lt;code&gt;request.cf&lt;/code&gt; object, populated at the edge before your code even runs. No lookup, no latency, no key. Here's what's inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;asn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;34984&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;// ISP's autonomous system number&lt;/span&gt;
  &lt;span class="nx"&gt;asOrganization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Superonline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ISP name&lt;/span&gt;
  &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Istanbul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Istanbul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;continent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isEUCountry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// "1" if EU, undefined otherwise&lt;/span&gt;
  &lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;41.01380&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// string, not number!&lt;/span&gt;
  &lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;28.94970&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;postalCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;34000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Europe/Istanbul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;colo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;IST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;// which CF datacenter handled this&lt;/span&gt;
  &lt;span class="nx"&gt;clientTcpRtt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;// user's RTT to the edge, in ms&lt;/span&gt;
  &lt;span class="nx"&gt;httpProtocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HTTP/3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;tlsVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TLSv1.3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;tlsCipher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AEAD-AES128-GCM-SHA256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last group surprised me most: you get the user's &lt;strong&gt;HTTP protocol, TLS version, and actual TCP round-trip time&lt;/strong&gt; for free. Try getting that from a geo API.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete IP endpoint in ~30 lines
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cf&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CF-Connecting-IP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&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="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;isp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;asOrganization&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;asn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;asn&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timezone&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpProtocol&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tlsVersion&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;rttMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientTcpRtt&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&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;That's the entire backend. No database, no GeoIP file to update monthly, no vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas (learned the hard way)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Coordinates are strings.&lt;/strong&gt; &lt;code&gt;latitude: "41.01380"&lt;/code&gt; — parse them or your JSON consumers will suffer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Every field can be missing.&lt;/strong&gt; Tor exits, some corporate proxies, and brand-new IP ranges come through with sparse data. Null-check everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;isEUCountry&lt;/code&gt; is the string &lt;code&gt;"1"&lt;/code&gt; or undefined.&lt;/strong&gt; Not a boolean. You've been warned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Local dev gives you an empty object.&lt;/strong&gt; &lt;code&gt;wrangler dev&lt;/code&gt; (and Next.js dev servers behind adapters) won't populate most fields. Guard for it, and test geo logic in preview deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. City-level accuracy is "usually right, sometimes hilarious."&lt;/strong&gt; Same as every IP geolocation source, this data comes from the same underlying registries. Country is reliable; city is best-effort. Don't build anything that &lt;em&gt;requires&lt;/em&gt; city precision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy angle
&lt;/h2&gt;

&lt;p&gt;This architecture has a property I didn't fully appreciate until I wrote the privacy policy: &lt;strong&gt;the user's IP never leaves the request path.&lt;/strong&gt; There's no third-party geo provider receiving your traffic logs as a side effect. Combined with not writing logs yourself, you can honestly say: nothing is stored, nothing is shared. That's the whole privacy section.&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://whatsmy.fyi" rel="noopener noreferrer"&gt;whatsmy.fyi&lt;/a&gt; this is the entire data pipeline: the site, the &lt;a href="https://whatsmy.fyi/docs" rel="noopener noreferrer"&gt;free API&lt;/a&gt;, all of it runs on &lt;code&gt;request.cf&lt;/code&gt; and nothing else. Sub-50ms TTFB, zero external calls, zero logs.&lt;/p&gt;

&lt;p&gt;If you're on Workers and calling a geo API today, check &lt;code&gt;request.cf&lt;/code&gt; first. The data was already there before you asked.&lt;/p&gt;

</description>
      <category>cloudflarechallenge</category>
      <category>serverless</category>
      <category>webdev</category>
      <category>privacy</category>
    </item>
    <item>
      <title>I built a "what is my IP" site because I was tired of the ugly ones</title>
      <dc:creator>Koray KÖYLÜ</dc:creator>
      <pubDate>Thu, 28 May 2026 15:39:14 +0000</pubDate>
      <link>https://dev.to/koraykoylu/i-built-a-what-is-my-ip-site-because-i-was-tired-of-the-ugly-ones-263j</link>
      <guid>https://dev.to/koraykoylu/i-built-a-what-is-my-ip-site-because-i-was-tired-of-the-ugly-ones-263j</guid>
      <description>&lt;p&gt;I use "what is my IP" sites maybe once a month. Every time I end up on something covered in ads, calling three different tracking APIs, and showing me results I don't fully understand. So I spent a weekend building whatsmy.fyi.&lt;/p&gt;

&lt;p&gt;The thing I didn't expect: you don't need an IP geolocation  API at all if you're on Cloudflare Workers. Every request  comes with a &lt;code&gt;cf&lt;/code&gt; object that already has your city, country,  ISP, TLS version, HTTP protocol, and RTT. Free. Zero latency.&lt;/p&gt;

&lt;p&gt;The part I enjoyed most was the WebRTC leak test. It checks  whether your browser is exposing your real IP through  RTCPeerConnection even when you're on a VPN. I ran it on  my own setup. It was leaking.&lt;/p&gt;

&lt;p&gt;Zero logs. Zero storage. Just your data, shown to you.&lt;/p&gt;

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

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>cloudflare</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
