<?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: Mathis Higuinen</title>
    <description>The latest articles on DEV Community by Mathis Higuinen (@mathis_higuinen_6db9b244c).</description>
    <link>https://dev.to/mathis_higuinen_6db9b244c</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%2F3952777%2F20fc0059-d3d7-45bd-9098-6dd7587d49a0.png</url>
      <title>DEV Community: Mathis Higuinen</title>
      <link>https://dev.to/mathis_higuinen_6db9b244c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mathis_higuinen_6db9b244c"/>
    <language>en</language>
    <item>
      <title>Drop-in travel widgets — add visa rules, budgets &amp; passport scores to any page with one line</title>
      <dc:creator>Mathis Higuinen</dc:creator>
      <pubDate>Sat, 04 Jul 2026 23:56:01 +0000</pubDate>
      <link>https://dev.to/mathis_higuinen_6db9b244c/drop-in-travel-widgets-add-visa-rules-budgets-passport-scores-to-any-page-with-one-line-5eje</link>
      <guid>https://dev.to/mathis_higuinen_6db9b244c/drop-in-travel-widgets-add-visa-rules-budgets-passport-scores-to-any-page-with-one-line-5eje</guid>
      <description>&lt;p&gt;A suite of embeddable widgets that pull live visa requirements, travel budgets, passport scores and country comparisons — no API wiring, no keys, one script tag. With a built-in affiliate parameter so your travel content can earn.&lt;/p&gt;

&lt;p&gt;If you've ever run a travel blog, a nomad resource site, or a "moving abroad" landing page, you've hit the same wall I did: &lt;strong&gt;travel data is annoying to build.&lt;/strong&gt; Visa rules change per passport/destination pair, budgets vary by city, and passport rankings shift constantly. You either scrape it, maintain it by hand, or wire up against an API and babysit the integration.&lt;/p&gt;

&lt;p&gt;I got tired of that, so I built &lt;a href="https://embed.orizn.app/" rel="noopener noreferrer"&gt;&lt;strong&gt;embed.orizn.app&lt;/strong&gt;&lt;/a&gt; — a set of copy-paste widgets that render live travel data on any page. No backend, no API keys, no build step. You drop a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; and a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, and you get an interactive component fed by the &lt;a href="https://orizn.app/" rel="noopener noreferrer"&gt;Orizn&lt;/a&gt; dataset (199 countries, 32 data points per passport/destination pair, 15 languages).&lt;/p&gt;

&lt;p&gt;Here's how each one works.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Visa requirements
&lt;/h2&gt;

&lt;p&gt;Shows entry requirements for a destination, adapting to the visitor's passport. Point it at a destination country and it does the rest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"orizn-visa"&lt;/span&gt;
     &lt;span class="na"&gt;data-destination=&lt;/span&gt;&lt;span class="s"&gt;"VNM"&lt;/span&gt;
     &lt;span class="na"&gt;data-affiliate=&lt;/span&gt;&lt;span class="s"&gt;"aff_e23154b8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://visa.orizn.app/embed/visa.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;data-destination&lt;/code&gt; — ISO 3166-1 alpha-3 code of where they're going (&lt;code&gt;VNM&lt;/code&gt; = Vietnam)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-affiliate&lt;/code&gt; — your affiliate ID (more on that below)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Travel budget
&lt;/h2&gt;

&lt;p&gt;A quick cost-of-travel snapshot for a destination — the kind of "can I afford this?" widget that keeps readers on the page instead of bouncing to a search engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"orizn-budget"&lt;/span&gt;
     &lt;span class="na"&gt;data-destination=&lt;/span&gt;&lt;span class="s"&gt;"VNM"&lt;/span&gt;
     &lt;span class="na"&gt;data-affiliate=&lt;/span&gt;&lt;span class="s"&gt;"aff_e23154b8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://visa.orizn.app/embed/budget.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Passport score
&lt;/h2&gt;

&lt;p&gt;Rank and reach of a given passport. Great for a "how powerful is your passport?" section, or as a hook on a citizenship/relocation page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"orizn-score"&lt;/span&gt;
     &lt;span class="na"&gt;data-passport=&lt;/span&gt;&lt;span class="s"&gt;"FRA"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://visa.orizn.app/embed/score.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;data-passport&lt;/code&gt; — ISO alpha-3 of the passport (&lt;code&gt;FRA&lt;/code&gt; = France)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Country comparison
&lt;/h2&gt;

&lt;p&gt;Side-by-side of two destinations for a single passport — visa treatment, and the differences that actually matter when someone's deciding between two places.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"orizn-compare"&lt;/span&gt;
     &lt;span class="na"&gt;data-country1=&lt;/span&gt;&lt;span class="s"&gt;"VNM"&lt;/span&gt;
     &lt;span class="na"&gt;data-country2=&lt;/span&gt;&lt;span class="s"&gt;"THA"&lt;/span&gt;
     &lt;span class="na"&gt;data-passport=&lt;/span&gt;&lt;span class="s"&gt;"FRA"&lt;/span&gt;
     &lt;span class="na"&gt;data-affiliate=&lt;/span&gt;&lt;span class="s"&gt;"aff_e23154b8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://visa.orizn.app/embed/compare.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;data-country1&lt;/code&gt; / &lt;code&gt;data-country2&lt;/code&gt; — the two destinations to compare&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-passport&lt;/code&gt; — the passport to evaluate them against&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Trip checklist
&lt;/h2&gt;

&lt;p&gt;A ready-to-go checklist tailored to a specific passport/destination pair — what to prepare before departure (documents, entry conditions, and the things travelers forget). This is the "actionable" widget: it turns your page from information into a to-do list readers can actually follow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"orizn-checklist"&lt;/span&gt;
     &lt;span class="na"&gt;data-destination=&lt;/span&gt;&lt;span class="s"&gt;"VNM"&lt;/span&gt;
     &lt;span class="na"&gt;data-passport=&lt;/span&gt;&lt;span class="s"&gt;"FRA"&lt;/span&gt;
     &lt;span class="na"&gt;data-affiliate=&lt;/span&gt;&lt;span class="s"&gt;"aff_e23154b8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://visa.orizn.app/embed/checklist.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;data-destination&lt;/code&gt; — where they're going&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-passport&lt;/code&gt; — the passport they're traveling on&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-affiliate&lt;/code&gt; — your affiliate ID&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Notice the &lt;code&gt;data-affiliate&lt;/code&gt; attribute on most of these. That's the part I'm most excited about for content creators.&lt;/p&gt;

&lt;p&gt;Every widget carries your affiliate ID, so the traffic you send through it is attributed to you. If you run a travel blog, a relocation newsletter, or a nomad community, you can embed genuinely useful tools &lt;strong&gt;and&lt;/strong&gt; earn from the referrals they generate — instead of the widget being a pure cost center on your site. Grab your ID from &lt;a href="https://embed.orizn.app/" rel="noopener noreferrer"&gt;embed.orizn.app&lt;/a&gt; and swap &lt;code&gt;aff_e23154b8&lt;/code&gt; for your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try them live
&lt;/h2&gt;

&lt;p&gt;Since this article is being read on a platform that (rightly) strips arbitrary &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags for security, the snippets above render as &lt;strong&gt;code&lt;/strong&gt;, not as live components. To see the widgets actually running — and to configure your own with your affiliate ID — head to the live playground:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://embed.orizn.app/" rel="noopener noreferrer"&gt;embed.orizn.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything is ISO-code driven, loads &lt;code&gt;async&lt;/code&gt; so it won't block your page, and works with plain HTML — no framework required. Drop it into WordPress, a static site, Webflow, a landing page, whatever you've got.&lt;/p&gt;

&lt;p&gt;If you build something with it, I'd love to see it — drop a link in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Your rate limiter is not a security control</title>
      <dc:creator>Mathis Higuinen</dc:creator>
      <pubDate>Sat, 04 Jul 2026 01:14:47 +0000</pubDate>
      <link>https://dev.to/mathis_higuinen_6db9b244c/your-rate-limiter-is-not-a-security-control-4b3f</link>
      <guid>https://dev.to/mathis_higuinen_6db9b244c/your-rate-limiter-is-not-a-security-control-4b3f</guid>
      <description>&lt;p&gt;Last week a bot pulled ~100% of one passport's data out of my visa API in a few minutes. My rate limiter — 3,000 calls/month on the free tier — never fired once. It didn't need to: a full passport is only ~195 calls.&lt;/p&gt;

&lt;p&gt;That's the whole point of this post. Rate limiting protects your infrastructure cost. It does not protect your data. They're two different problems, and conflating them is how solo builders leak their most valuable asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why volume limits fail against extraction
&lt;/h2&gt;

&lt;p&gt;A rate limiter answers one question: is this client costing me too much per unit time? Targeted extraction is specifically designed to stay under that line. My scraper ran at ~25 req/min — trivial load — and would have needed only ~40,000 calls total to copy everything. Spread across a few free accounts, that's invisible to any per-key volume cap.&lt;/p&gt;

&lt;p&gt;Volume is the wrong axis. The right axes are identity, coverage, and shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defense 1 — Log the network, not just the key
&lt;/h2&gt;

&lt;p&gt;The first thing that bit me: I could disable the API key, but I hadn't stored the source IP, so re-signup was free. Fix the log first.&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;visa_api_logs&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_visa_logs_ip&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;visa_api_logs&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="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Populate &lt;code&gt;ip&lt;/code&gt; from &lt;code&gt;cf-connecting-ip&lt;/code&gt; (the real client behind Cloudflare — never trust &lt;code&gt;remote_addr&lt;/code&gt; at the edge). Now a repeat offender is blockable at the network layer, and you can feed the IP straight to a Cloudflare firewall rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defense 2 — Cap the resource, not the request count
&lt;/h2&gt;

&lt;p&gt;Instead of "N requests/month," cap "N distinct destinations per passport per day." A legitimate user checks a handful of destinations for their own passport. A scraper wants all of them. The cap barely touches real usage and hard-stops a sweep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defense 3 — Detect the shape in real time
&lt;/h2&gt;

&lt;p&gt;The sweep has a signature: dense, sequential, exhaustive coverage of a single &lt;code&gt;from&lt;/code&gt;. That's detectable per key in a sliding window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sketch: flag a key sweeping one passport across many destinations.&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_dump_pattern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VisaHit&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&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;let&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;
        &lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="py"&gt;.ts&lt;/span&gt;&lt;span class="nf"&gt;.elapsed&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;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Group by source passport, count distinct destinations.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;by_passport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&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;HashSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;HashMap&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&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;h&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;by_passport&lt;/span&gt;&lt;span class="nf"&gt;.entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="py"&gt;.from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.or_default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="py"&gt;.to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// One passport, many destinations, tight cadence = extraction.&lt;/span&gt;
    &lt;span class="n"&gt;by_passport&lt;/span&gt;
        &lt;span class="nf"&gt;.values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.any&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;dests&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;dests&lt;/span&gt;&lt;span class="nf"&gt;.len&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;DUMP_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;mean_interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&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;Trip it → throttle that key hard (or 402 it) and log the IP. You're now reacting to behavior, not just counting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;Split the two jobs explicitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiter&lt;/strong&gt; → "don't cost me money." Cheap, coarse, per-key volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extraction defense&lt;/strong&gt; → "don't copy my dataset." Identity + coverage + cadence, network-aware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only have the first one, you have a billing guardrail dressed up as security. Ask me how I know.&lt;/p&gt;

</description>
      <category>security</category>
      <category>api</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Anatomy of an API scrape: reading 251 requests like a crime scene</title>
      <dc:creator>Mathis Higuinen</dc:creator>
      <pubDate>Fri, 03 Jul 2026 00:49:23 +0000</pubDate>
      <link>https://dev.to/mathis_higuinen_6db9b244c/anatomy-of-an-api-scrape-reading-251-requests-like-a-crime-scene-2o93</link>
      <guid>https://dev.to/mathis_higuinen_6db9b244c/anatomy-of-an-api-scrape-reading-251-requests-like-a-crime-scene-2o93</guid>
      <description>&lt;p&gt;Last week someone tried to copy my visa API's database. They didn't succeed — they got 0.6% of it before I cut the key — but the 251 requests they left behind are a near-perfect teaching case for what targeted API extraction actually looks like from the defender's side.&lt;/p&gt;

&lt;p&gt;Here's the forensic walkthrough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The target
&lt;/h2&gt;

&lt;p&gt;One endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/v1/visa?from={passport}&amp;amp;to={destination}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns the visa rule for a passport→destination pair — visa type, allowed stay, conditions. The full matrix is &lt;strong&gt;~39,585 pairs&lt;/strong&gt;. That matrix is the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evidence
&lt;/h2&gt;

&lt;p&gt;The attacker's requests weren't spread across the map. They were a sweep, one passport at a time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Passport&lt;/th&gt;
&lt;th&gt;Destinations pulled&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🇦🇪 UAE (ARE)&lt;/td&gt;
&lt;td&gt;195&lt;/td&gt;
&lt;td&gt;~100% of that passport's matrix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇦🇺 Australia (AUS)&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;~1/4, interrupted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇨🇳 China (CHN)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;test calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;249 unique pairs, near-zero duplicates. Whoever wrote this was methodical: validate that one full passport comes out cleanly, then move to the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the cadence
&lt;/h2&gt;

&lt;p&gt;The timestamps are where a scrape gives itself away. Minute by minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;11:56   2   ← test phase (incl. the one failure)
11:57   1
11:58   25  ┐
11:59   26  │
12:00   20  │  ~25 req/min, dead regular
  …         │  = one request every ~2.4s
12:07   21  ┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No human reads visa rules on a 2.4-second metronome for 11 minutes. This is a loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fingerprint
&lt;/h2&gt;

&lt;p&gt;Four signals — and the point isn't nationality, it's that the request parameters themselves leaked the intent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Handle:&lt;/strong&gt; &lt;code&gt;visadb_scraper&lt;/code&gt;. It signed its own work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email:&lt;/strong&gt; throwaway &lt;code&gt;@temp.com&lt;/code&gt;. No intention of receiving anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Languages:&lt;/strong&gt; &lt;code&gt;en&lt;/code&gt; + &lt;code&gt;zh&lt;/code&gt;, on a product with no Chinese-market surface yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error signature:&lt;/strong&gt; the very first call (CHN→THA, in Chinese, 11:56:45) failed, then everything ran clean. Classic "calibrating the script" tell.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The math
&lt;/h2&gt;

&lt;p&gt;250 records is 0.6% of the base. At 25 req/min, a full dump would've taken &lt;strong&gt;~26 hours&lt;/strong&gt;. This wasn't a dump — it was a &lt;strong&gt;feasibility test&lt;/strong&gt;. They proved a whole passport comes out easily, then stopped, nowhere near the 3,000/month free-tier ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I couldn't see
&lt;/h2&gt;

&lt;p&gt;I blocked the key (&lt;code&gt;active=false&lt;/code&gt;) and the sweep stopped. But my request logs didn't capture the source IP — so I could block the key, not the person. Re-signup costs nothing.&lt;/p&gt;

&lt;p&gt;That gap is the real lesson, and it's the subject of the next two posts: &lt;strong&gt;rate limits are a cost control, not a security control&lt;/strong&gt; — and &lt;strong&gt;if your logs can't see the network, you can't defend at it.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;If you're shipping a data product: assume the first person who finds it valuable will be the first person who tries to copy it. Instrument for that on day one.&lt;/p&gt;

</description>
      <category>security</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Chrome Extension That Shows Visa Requirements Before You Book</title>
      <dc:creator>Mathis Higuinen</dc:creator>
      <pubDate>Tue, 16 Jun 2026 04:14:31 +0000</pubDate>
      <link>https://dev.to/mathis_higuinen_6db9b244c/building-a-chrome-extension-that-shows-visa-requirements-before-you-book-4bpl</link>
      <guid>https://dev.to/mathis_higuinen_6db9b244c/building-a-chrome-extension-that-shows-visa-requirements-before-you-book-4bpl</guid>
      <description>&lt;p&gt;description: How Orizn brings visa requirements directly into Google Flights, Kayak, Skyscanner, and other travel booking flows.&lt;br&gt;
tags: traveltech, chromeextension, startup, buildinpublic&lt;/p&gt;

&lt;p&gt;Booking a flight is exciting.&lt;/p&gt;

&lt;p&gt;Until one question suddenly appears:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I actually enter this country?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is where the friction starts.&lt;/p&gt;

&lt;p&gt;You open Google Flights.&lt;br&gt;
You compare prices, dates, airlines, routes, layovers.&lt;/p&gt;

&lt;p&gt;Then you leave the booking flow.&lt;/p&gt;

&lt;p&gt;One tab for the flight.&lt;br&gt;
One tab for visa requirements.&lt;br&gt;
One tab for official government sources.&lt;br&gt;
One tab for travel blogs.&lt;br&gt;
Maybe one more tab for Reddit, just to be sure.&lt;/p&gt;

&lt;p&gt;And after 20 minutes, you are still not completely confident.&lt;/p&gt;

&lt;p&gt;That exact pain is why we built the &lt;strong&gt;Orizn Chrome Extension&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://extension.orizn.app" rel="noopener noreferrer"&gt;extension.orizn.app&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Visa information exists.&lt;/p&gt;

&lt;p&gt;But it rarely appears at the moment when travelers need it most.&lt;/p&gt;

&lt;p&gt;Most people discover visa rules too late:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;after finding a cheap flight&lt;/li&gt;
&lt;li&gt;after planning an itinerary&lt;/li&gt;
&lt;li&gt;after booking accommodation&lt;/li&gt;
&lt;li&gt;sometimes even after buying the ticket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That does not make sense.&lt;/p&gt;

&lt;p&gt;Visa requirements should not be an afterthought.&lt;/p&gt;

&lt;p&gt;They should appear exactly where the travel decision is being made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea behind Orizn Extension
&lt;/h2&gt;

&lt;p&gt;While traveling, I kept seeing the same pattern again and again.&lt;/p&gt;

&lt;p&gt;People were not only asking:&lt;/p&gt;

&lt;p&gt;“Where should I go?”&lt;/p&gt;

&lt;p&gt;They were also asking:&lt;/p&gt;

&lt;p&gt;The answer depends on the passport, the destination, the length of stay, and sometimes the type of entry authorization required.&lt;/p&gt;

&lt;p&gt;But travel platforms usually focus on flights, prices, hotels, and availability.&lt;/p&gt;

&lt;p&gt;The visa layer is missing.&lt;/p&gt;

&lt;p&gt;So we built Orizn Extension to bring that layer directly into the booking experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1036n7olqidday5qtbe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1036n7olqidday5qtbe.png" alt=" " width="800" height="451"&gt;&lt;/a&gt; “Where can I actually go with my passport?”&lt;/p&gt;

&lt;p&gt;The extension runs in the background while you browse travel websites such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Flights&lt;/li&gt;
&lt;li&gt;Kayak&lt;/li&gt;
&lt;li&gt;Skyscanner&lt;/li&gt;
&lt;li&gt;and 30+ other travel platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You set your passport once.&lt;/p&gt;

&lt;p&gt;Then, when you search for a destination, Orizn instantly shows useful entry information before you book.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether you need a visa&lt;/li&gt;
&lt;li&gt;how long you can stay visa-free&lt;/li&gt;
&lt;li&gt;key travel requirements&lt;/li&gt;
&lt;li&gt;useful destination context&lt;/li&gt;
&lt;li&gt;important information to check before confirming your trip&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No extra tabs.&lt;br&gt;
No unnecessary confusion.&lt;br&gt;
No last-minute surprise.&lt;/p&gt;

&lt;p&gt;Just the right information, at the right moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Chrome extension?
&lt;/h2&gt;

&lt;p&gt;We could have built only a standalone visa checker.&lt;/p&gt;

&lt;p&gt;Actually, we already built the infrastructure behind that.&lt;/p&gt;

&lt;p&gt;I wrote about it here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/mathis_higuinen_6db9b244c/building-a-multilingual-api-serving-39585-visa-pairs-in-15-languages-1bcm"&gt;Building a multilingual API serving 39,585 visa pairs in 15 languages&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That API is the foundation.&lt;/p&gt;

&lt;p&gt;But the extension solves a different problem.&lt;/p&gt;

&lt;p&gt;A visa checker answers a question when the user decides to search for it.&lt;/p&gt;

&lt;p&gt;A browser extension answers the question before the user forgets to ask it.&lt;/p&gt;

&lt;p&gt;That difference matters.&lt;/p&gt;

&lt;p&gt;Because when someone is booking a trip, they are already focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;price&lt;/li&gt;
&lt;li&gt;dates&lt;/li&gt;
&lt;li&gt;destination&lt;/li&gt;
&lt;li&gt;luggage&lt;/li&gt;
&lt;li&gt;layovers&lt;/li&gt;
&lt;li&gt;timing&lt;/li&gt;
&lt;li&gt;availability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Visa rules are critical, but they are not always top of mind.&lt;/p&gt;

&lt;p&gt;So instead of asking travelers to change their behavior, we decided to meet them where they already are.&lt;/p&gt;

&lt;p&gt;Inside the booking flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product experience
&lt;/h2&gt;

&lt;p&gt;The goal was to make the extension feel invisible until it becomes useful.&lt;/p&gt;

&lt;p&gt;The user should not have to open a dashboard every time.&lt;/p&gt;

&lt;p&gt;They should not have to manually search every destination.&lt;/p&gt;

&lt;p&gt;They should not have to copy and paste country names into another tool.&lt;/p&gt;

&lt;p&gt;The flow is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the extension&lt;/li&gt;
&lt;li&gt;Select your passport&lt;/li&gt;
&lt;li&gt;Browse travel websites normally&lt;/li&gt;
&lt;li&gt;Get visa and destination context when it matters&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is it.&lt;/p&gt;

&lt;p&gt;The extension becomes a small decision layer on top of existing travel platforms.&lt;/p&gt;

&lt;p&gt;Not another travel app to manage.&lt;/p&gt;

&lt;p&gt;Not another tab to keep open.&lt;/p&gt;

&lt;p&gt;Just a contextual assistant for international travel decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger vision
&lt;/h2&gt;

&lt;p&gt;Orizn is not only a Chrome extension.&lt;/p&gt;

&lt;p&gt;The extension is one part of a larger travel infrastructure ecosystem.&lt;/p&gt;

&lt;p&gt;The idea is to build tools that make travel planning clearer, safer, and more intelligent.&lt;/p&gt;

&lt;p&gt;That includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;visa data&lt;/li&gt;
&lt;li&gt;travel APIs&lt;/li&gt;
&lt;li&gt;browser tools&lt;/li&gt;
&lt;li&gt;destination intelligence&lt;/li&gt;
&lt;li&gt;developer SDKs&lt;/li&gt;
&lt;li&gt;AI agent integrations&lt;/li&gt;
&lt;li&gt;multilingual travel infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extension is the consumer-facing layer.&lt;/p&gt;

&lt;p&gt;The API is the infrastructure layer.&lt;/p&gt;

&lt;p&gt;Together, they make visa information available both to travelers and to developers building travel products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;International travel is becoming more complex.&lt;/p&gt;

&lt;p&gt;Visa-free access changes.&lt;br&gt;
Electronic travel authorizations are expanding.&lt;br&gt;
Border policies evolve.&lt;br&gt;
Some countries change entry rules quickly.&lt;br&gt;
Travelers often rely on outdated blogs or fragmented information.&lt;/p&gt;

&lt;p&gt;That creates risk.&lt;/p&gt;

&lt;p&gt;Not just inconvenience.&lt;/p&gt;

&lt;p&gt;A traveler can book the perfect flight and still be denied boarding if they missed an entry requirement.&lt;/p&gt;

&lt;p&gt;That is the kind of problem we want to reduce.&lt;/p&gt;

&lt;p&gt;Not by replacing official sources, but by making important information easier to notice earlier in the decision process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building next
&lt;/h2&gt;

&lt;p&gt;The current version focuses on surfacing visa and destination context while browsing travel websites.&lt;/p&gt;

&lt;p&gt;Next, we want to improve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more travel website coverage&lt;/li&gt;
&lt;li&gt;better destination detection&lt;/li&gt;
&lt;li&gt;richer country context&lt;/li&gt;
&lt;li&gt;clearer requirement explanations&lt;/li&gt;
&lt;li&gt;smoother onboarding&lt;/li&gt;
&lt;li&gt;deeper integration with the Orizn Visa API&lt;/li&gt;
&lt;li&gt;more personalized travel recommendations based on passport and destination&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The long-term goal is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you are about to book a trip, Orizn should help you understand if that trip is actually possible for your passport.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Booking a flight should feel exciting.&lt;/p&gt;

&lt;p&gt;Not uncertain.&lt;/p&gt;

&lt;p&gt;Visa requirements should not be discovered after the booking.&lt;/p&gt;

&lt;p&gt;They should appear before the decision is made.&lt;/p&gt;

&lt;p&gt;That is why we built the Orizn Chrome Extension.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://extension.orizn.app" rel="noopener noreferrer"&gt;Try Orizn Extension&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are building in travel tech, browser extensions, APIs, or AI agents, I would love to connect and share more about the infrastructure behind Orizn.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building a multilingual API serving 39,585 visa pairs in 15 languages</title>
      <dc:creator>Mathis Higuinen</dc:creator>
      <pubDate>Tue, 26 May 2026 23:34:53 +0000</pubDate>
      <link>https://dev.to/mathis_higuinen_6db9b244c/building-a-multilingual-api-serving-39585-visa-pairs-in-15-languages-1bcm</link>
      <guid>https://dev.to/mathis_higuinen_6db9b244c/building-a-multilingual-api-serving-39585-visa-pairs-in-15-languages-1bcm</guid>
      <description>&lt;p&gt;How we structured a visa requirements database across 15 languages with ISO standardization, caching layers, and translation pipelines.&lt;/p&gt;

&lt;p&gt;Most travel APIs return data in English. That's fine if your users are American or British. It's useless if they're Japanese, Thai, Arabic-speaking, or any of the 5.5 billion people whose first language isn't English.&lt;/p&gt;

&lt;p&gt;When a Vietnamese traveler looks up visa requirements for France, they need the answer in Vietnamese — not just the visa type, but the required documents list, the application process, the embassy information, the travel tips. Translating "Valid passport with 6 months minimum validity" into 15 languages isn't a string replacement. It's a content localization problem at scale.&lt;/p&gt;

&lt;p&gt;The Orizn Visa API serves 39,585 passport-destination pairs in 15 languages: English, French, Spanish, Portuguese, German, Japanese, Korean, Chinese, Russian, Italian, Arabic, Hindi, Thai, Vietnamese, and Filipino. Here's how the system is structured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these 15 languages
&lt;/h2&gt;

&lt;p&gt;Not random. These 15 cover approximately 75% of the world's internet users by primary language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;English&lt;/strong&gt; — lingua franca, baseline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chinese&lt;/strong&gt; — 1.1B speakers, China's visa-free program expanding rapidly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spanish&lt;/strong&gt; — 550M speakers, Latin America is a growing travel market&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hindi&lt;/strong&gt; — 600M speakers, Indian passport holders are one of the most visa-restricted populations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arabic&lt;/strong&gt; — 370M speakers, Gulf states have some of the fastest-growing passports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portuguese&lt;/strong&gt; — 260M speakers, Brazil alone has 210M people&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;French&lt;/strong&gt; — 280M speakers, major passport in Africa and Europe&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Japanese, Korean&lt;/strong&gt; — high-value travel markets, strong outbound tourism&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Russian&lt;/strong&gt; — 250M speakers, complex visa landscape post-2022&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;German, Italian&lt;/strong&gt; — core EU passports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thai, Vietnamese, Filipino&lt;/strong&gt; — Southeast Asia, where digital nomads concentrate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each language isn't just a translation — it's a market. A Thai translation means Thai travel bloggers can embed our widgets and Thai developers can build apps with localized visa data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data architecture
&lt;/h2&gt;

&lt;p&gt;The core data model is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;visa_pair {
  passport:     ISO 3166-1 alpha-3  (e.g. "FRA")
  destination:  ISO 3166-1 alpha-3  (e.g. "JPN")
  requirement:  enum                (visa_free | visa_required | e_visa |
                                     visa_on_arrival | eta | no_admission)
  visa_free_days: integer | null
  verified:     boolean
  source_url:   string
  last_updated: timestamp
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;199 passports × 199 destinations = 39,601 theoretical pairs. Some pairs are self-referential (you don't need a visa to visit your own country), bringing the actual count to 39,585.&lt;/p&gt;

&lt;p&gt;Each pair has a base record in English. Translations are stored separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;visa_translation {
  passport:     ISO3
  destination:  ISO3
  lang:         enum (15 values)
  description:  text
  documents:    text[]
  process:      text[]
  tips:         text[]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;39,585 pairs × 15 languages = &lt;strong&gt;593,775 translation records&lt;/strong&gt;. That's the real scale of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The translation pipeline
&lt;/h2&gt;

&lt;p&gt;Raw visa data comes from 136 government portals. Most publish in their national language plus English. Some only publish in their national language.&lt;/p&gt;

&lt;p&gt;The pipeline has 5 stages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Extraction&lt;/strong&gt; — Pull structured visa rules from government sources. This is the hardest part. Every government formats their visa information differently. Some have clean REST APIs. Most have PDF documents or HTML pages with inconsistent formatting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Normalization&lt;/strong&gt; — Map to the 6 standardized requirement types. A government might say "no visa needed for stays under 90 days" — that maps to &lt;code&gt;visa_free&lt;/code&gt; with &lt;code&gt;visa_free_days: 90&lt;/code&gt;. Another might say "electronic authorization required prior to travel" — that's &lt;code&gt;eta&lt;/code&gt;. The mapping isn't always obvious and edge cases are everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. English baseline&lt;/strong&gt; — Generate the English version with all fields: description, documents_required, process, tips, country_info. This is the canonical record that everything else derives from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Translation&lt;/strong&gt; — Generate the 14 other language versions. This isn't word-for-word translation. Document names, process steps, and tips need to be culturally adapted. "Apply at the embassy" in Japanese includes the Japanese name of the embassy and Japanese-language application forms. "Proof of sufficient funds" in Arabic needs to reflect local banking norms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Verification&lt;/strong&gt; — Cross-check against at least 2 independent sources per pair. The &lt;code&gt;verified&lt;/code&gt; flag indicates whether the data has been confirmed. Unverified pairs are still returned but flagged — better to have data with a caveat than no data at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching strategy
&lt;/h2&gt;

&lt;p&gt;With 593K+ records and 15 language variants, caching is critical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request flow:
Client → API Gateway → Cache (Redis) → Database

Cache key pattern: visa:{passport}:{destination}:{lang}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TTL strategy is split by volatility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;24 hours&lt;/strong&gt; for stable pairs — visa types don't change hourly. A &lt;code&gt;visa_free&lt;/code&gt; pair that's been stable for 3 years doesn't need real-time freshness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 hour&lt;/strong&gt; for recently changed pairs — if Thailand just modified its policy, the cache needs to reflect that quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cache&lt;/strong&gt; for the &lt;code&gt;/changes&lt;/code&gt; endpoint — it queries the diff table directly. When someone asks "what changed this week?", they need the latest data, not a cached snapshot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;/check&lt;/code&gt; endpoint (quick check, no documents) is cached aggressively — it returns 5 fields. The &lt;code&gt;/visa&lt;/code&gt; endpoint (full details with documents, process, tips) has shorter TTLs because embassies can update document requirements at any time.&lt;/p&gt;

&lt;h2&gt;
  
  
  ISO standardization decisions
&lt;/h2&gt;

&lt;p&gt;We use ISO 3166-1 alpha-3 exclusively. Not alpha-2 (&lt;code&gt;FR&lt;/code&gt;, &lt;code&gt;JP&lt;/code&gt;), not country names (&lt;code&gt;France&lt;/code&gt;, &lt;code&gt;Japan&lt;/code&gt;), not IATA codes (&lt;code&gt;CDG&lt;/code&gt;, &lt;code&gt;NRT&lt;/code&gt;). Three reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unambiguous&lt;/strong&gt; — alpha-3 has no collisions across all 199 countries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Universal&lt;/strong&gt; — same codes work regardless of language (a Japanese developer sends &lt;code&gt;FRA&lt;/code&gt;, not &lt;code&gt;フランス&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machine-readable&lt;/strong&gt; — 3 uppercase ASCII characters, trivially validatable with a regex&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API auto-uppercases inputs (&lt;code&gt;fra&lt;/code&gt; → &lt;code&gt;FRA&lt;/code&gt;) and returns clear error messages for invalid codes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;passport&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; value &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;JP&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; is not a valid ISO 3166-1 alpha-3 code. Did you mean &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;JPN&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;?"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters especially for MCP and agent usage where the LLM might send lowercase or alpha-2 codes. A clear error message lets the agent self-correct and retry.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Start with fewer languages.&lt;/strong&gt; Launching with 15 simultaneously was ambitious. Starting with 5 (English, Spanish, French, Chinese, Arabic) and adding based on demand would have been faster and let us focus quality on the highest-impact languages first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invest in government source monitoring earlier.&lt;/strong&gt; The hardest operational challenge isn't translation — it's knowing when a government changes its visa policy. Thailand's 60→30 day rollback happened via a cabinet resolution. That's not an RSS feed. Building automated monitoring for 136 government portals is an ongoing project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the diff system from day one.&lt;/strong&gt; We added the &lt;code&gt;/changes&lt;/code&gt; endpoint later. If we'd built temporal versioning into the data model from the start (&lt;code&gt;valid_from&lt;/code&gt;, &lt;code&gt;valid_to&lt;/code&gt; on every record), the change detection and audit trail features would have been trivial instead of retrofitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the API
&lt;/h2&gt;

&lt;p&gt;The multilingual response in action:&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;# English (free, no API key)&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://visa.orizn.app/api/v1/visa/check?passport=JPN&amp;amp;destination=FRA"&lt;/span&gt;

&lt;span class="c"&gt;# Japanese (needs free API key)&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://visa.orizn.app/api/v1/visa?passport=JPN&amp;amp;destination=FRA&amp;amp;lang=ja"&lt;/span&gt;

&lt;span class="c"&gt;# Arabic&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://visa.orizn.app/api/v1/visa?passport=JPN&amp;amp;destination=FRA&amp;amp;lang=ar"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick checks are free, no API key needed. Full details in 15 languages with a free key (3,000 req/month) from &lt;a href="https://visa.orizn.app" rel="noopener noreferrer"&gt;visa.orizn.app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;SDKs: &lt;code&gt;npm install orizn&lt;/code&gt; · &lt;code&gt;pip install orizn&lt;/code&gt; · &lt;code&gt;cargo add orizn&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/MattJeff/orizn-visa-api" rel="noopener noreferrer"&gt;github.com/MattJeff/orizn-visa-api&lt;/a&gt;&lt;/p&gt;

</description>
      <category>apigateway</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>i18n</category>
    </item>
    <item>
      <title>Building an MCP Server for Visa Requirements in TypeScript</title>
      <dc:creator>Mathis Higuinen</dc:creator>
      <pubDate>Tue, 26 May 2026 15:15:10 +0000</pubDate>
      <link>https://dev.to/mathis_higuinen_6db9b244c/building-an-mcp-server-for-visa-requirements-in-typescript-165a</link>
      <guid>https://dev.to/mathis_higuinen_6db9b244c/building-an-mcp-server-for-visa-requirements-in-typescript-165a</guid>
      <description>&lt;h2&gt;
  
  
  What is MCP, and why should you care?
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol (MCP) is an open standard created by Anthropic that lets AI agents connect to external tools and data sources through a unified interface. Think of it as USB-C for AI: one protocol, any agent. Instead of building custom integrations for Claude, GPT, Gemini, and every other model, you build one MCP server and every compatible agent can use it.&lt;/p&gt;

&lt;p&gt;MCP defines a simple contract. Your server exposes &lt;strong&gt;tools&lt;/strong&gt; (functions the AI can call) and &lt;strong&gt;resources&lt;/strong&gt; (data the AI can read). The agent discovers what's available, reads the descriptions, and decides when to use them. The transport layer is pluggable -- stdio for local tools, SSE or HTTP for remote ones.&lt;/p&gt;

&lt;p&gt;The protocol is gaining traction fast. Claude Desktop, Cursor, Windsurf, and dozens of other clients already support it. If you have an API that AI agents should be able to use, wrapping it in MCP is one of the highest-leverage things you can do right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why visa requirements are perfect for MCP
&lt;/h2&gt;

&lt;p&gt;Here's a problem every travel-focused AI agent has: visa requirements. Ask an LLM whether a Brazilian passport holder needs a visa for Japan, and you'll get an answer -- but it might be wrong. Models rely on training data that's months or years old, and visa policies change constantly. Thailand just launched a new 60-day visa exemption. Turkey updated its e-visa rules. The model doesn't know.&lt;/p&gt;

&lt;p&gt;Visa data is structured, query-driven, and time-sensitive. It's exactly the kind of information that should come from a live API, not from parametric memory. An MCP server bridges that gap: the agent recognizes a visa question, calls the tool, and gets current data from a real database.&lt;/p&gt;

&lt;p&gt;That's what we built with the &lt;strong&gt;Orizn Visa MCP Server&lt;/strong&gt; -- a TypeScript MCP server backed by an API covering 39,585 passport-destination pairs in 15 languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;

&lt;p&gt;The stack is deliberately minimal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime&lt;/strong&gt;: Node.js 18+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP SDK&lt;/strong&gt;: &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; (v1.12+)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transport&lt;/strong&gt;: stdio (runs locally alongside the AI client)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Orizn Visa API at &lt;code&gt;https://visa.orizn.app/api/v1/visa&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server exposes &lt;strong&gt;5 tools&lt;/strong&gt; and &lt;strong&gt;2 resources&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;check_visa_requirement&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full visa details for a passport-destination pair&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;quick_visa_check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fast yes/no check (free, no API key)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_all_destinations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All destinations for one passport at once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_visa_changes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recent visa policy updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_coverage_stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Database coverage statistics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resources&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;visa://supported-languages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The 15 supported language codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;visa://country-codes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All 199 ISO3 country codes the API accepts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Code walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Server initialization
&lt;/h3&gt;

&lt;p&gt;MCP servers start with a &lt;code&gt;Server&lt;/code&gt; instance and a transport. We use &lt;code&gt;StdioServerTransport&lt;/code&gt;, which communicates over stdin/stdout -- the standard for local MCP servers:&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;Server&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="s2"&gt;@modelcontextprotocol/sdk/server/index.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StdioServerTransport&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="s2"&gt;@modelcontextprotocol/sdk/server/stdio.js&lt;/span&gt;&lt;span class="dl"&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;server&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;Server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orizn-visa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="na"&gt;resources&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;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transport&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;StdioServerTransport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;server&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="nx"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You register capabilities upfront -- &lt;code&gt;tools&lt;/code&gt; and &lt;code&gt;resources&lt;/code&gt; -- so the client knows what to expect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining tools
&lt;/h3&gt;

&lt;p&gt;Tools are defined with a name, description, and JSON Schema for inputs. Here's the core visa check tool:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;check_visa_requirement&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check visa requirements between any two countries. Returns visa type &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(visa-free, e-visa, visa required, etc.), allowed stay duration, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;required documents, step-by-step application process, and travel tips. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Covers 39,585 passport-destination pairs in 15 languages. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Use this tool when the user asks about visa rules, entry requirements, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or whether they need a visa to visit a country.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;passport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&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="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ISO 3166-1 alpha-3 code of the passport (e.g. 'FRA').&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&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;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ISO 3166-1 alpha-3 code of the destination (e.g. 'JPN').&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&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;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Language code for the response. Defaults to 'en'.&lt;/span&gt;&lt;span class="dl"&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;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;passport&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destination&lt;/span&gt;&lt;span class="dl"&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;The tool handler validates inputs and calls the API:&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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CallToolRequestSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&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;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;check_visa_requirement&lt;/span&gt;&lt;span class="dl"&gt;"&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;passport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateISO3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;passport&lt;/span&gt;&lt;span class="dl"&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;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateISO3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destination&lt;/span&gt;&lt;span class="dl"&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;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateLang&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lang&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;apiFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;passport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&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="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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="mi"&gt;2&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;span class="c1"&gt;// ... other tools&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;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;p&gt;Resources let the agent look up reference data without burning an API call. We expose two static resources -- supported languages and valid country codes:&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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ReadResourceRequestSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visa://supported-languages&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
          &lt;span class="na"&gt;uri&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SUPPORTED_LANGUAGES_RESOURCE&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="mi"&gt;2&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="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visa://country-codes&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
          &lt;span class="na"&gt;uri&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;COUNTRY_CODES_RESOURCE&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="mi"&gt;2&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;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, when the agent isn't sure of a country code, it can check the resource first instead of guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  API client with retry logic
&lt;/h3&gt;

&lt;p&gt;The API client handles timeouts, retries on 5xx errors, and fails fast on 4xx:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;apiFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requiresKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;lastError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_RETRIES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&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;controller&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;AbortController&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;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;REQUEST_TIMEOUT_MS&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&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="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&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="c1"&gt;// Don't retry client errors -- the request itself is wrong&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ErrorCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InvalidRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;`API returned &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;await&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;text&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// 5xx: retry&lt;/span&gt;
      &lt;span class="nx"&gt;lastError&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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`API returned &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;McpError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;lastError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ErrorCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InternalError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`API request failed after &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MAX_RETRIES&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; attempts: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastError&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;The distinction between 4xx and 5xx is important. A 400 means the agent sent bad parameters -- retrying won't help. A 503 means the server hiccupped -- retrying might.&lt;/p&gt;

&lt;h3&gt;
  
  
  ISO3 validation
&lt;/h3&gt;

&lt;p&gt;Country codes are validated against a hardcoded set of 199 codes before hitting the API. This catches the most common error pattern (the LLM sends "JP" instead of "JPN") at the MCP layer with a clear error message, rather than letting it propagate as a cryptic API error:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateISO3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;paramName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&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;upper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ISO3_COUNTRY_CODES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ErrorCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InvalidParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;paramName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" value "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" is not a valid ISO 3166-1 alpha-3 country code.`&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="nx"&gt;upper&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;h2&gt;
  
  
  Key decisions and lessons learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tool descriptions are your most important code.&lt;/strong&gt; The LLM reads them to decide &lt;em&gt;when&lt;/em&gt; to invoke your tool. Vague descriptions mean the agent won't call your tool when it should. We explicitly state what each tool returns, what it covers (39,585 pairs, 15 languages), and when to use it versus alternatives. This isn't documentation -- it's prompt engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;stdout is sacred.&lt;/strong&gt; In stdio transport, stdout carries MCP protocol messages. If you &lt;code&gt;console.log()&lt;/code&gt; anything, you'll corrupt the protocol stream and crash the connection. All our logging goes to stderr via &lt;code&gt;process.stderr.write()&lt;/code&gt;. This is the #1 mistake people make when building their first MCP server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free tier drives adoption.&lt;/strong&gt; The &lt;code&gt;quick_visa_check&lt;/code&gt; and &lt;code&gt;get_coverage_stats&lt;/code&gt; tools work without an API key. This means anyone can &lt;code&gt;npx orizn-visa-mcp&lt;/code&gt; and immediately test it. Zero friction. If they need full details, they add a key. But the first experience is instant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate before you fetch.&lt;/strong&gt; Catching "FR" vs "FRA" at the validation layer gives the agent a clear, actionable error ("not a valid ISO 3166-1 alpha-3 code") instead of a generic API 400. The agent can self-correct and retry with the right format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing and distribution
&lt;/h2&gt;

&lt;p&gt;Building the server is half the work. Getting it in front of developers is the other half.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;npm publish&lt;/strong&gt; -- This enables &lt;code&gt;npx orizn-visa-mcp&lt;/code&gt;, the fastest path from discovery to running server. Set the &lt;code&gt;bin&lt;/code&gt; field in &lt;code&gt;package.json&lt;/code&gt; and add the shebang (&lt;code&gt;#!/usr/bin/env node&lt;/code&gt;) to your entry point.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MCP registries&lt;/strong&gt; -- Submit to &lt;a href="https://smithery.ai" rel="noopener noreferrer"&gt;Smithery&lt;/a&gt;, &lt;a href="https://mcp.so" rel="noopener noreferrer"&gt;mcp.so&lt;/a&gt;, and &lt;a href="https://glama.ai/mcp/servers" rel="noopener noreferrer"&gt;Glama&lt;/a&gt;. These are where developers browse for MCP servers. A &lt;code&gt;smithery.yaml&lt;/code&gt; config file handles Smithery's hosted deployment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;README as marketing&lt;/strong&gt; -- Your README is your landing page. Include a clear one-liner, a feature table, installation instructions for every major client (Claude Desktop, Cursor, VS Code), and example outputs. Most developers decide whether to try your server in 30 seconds of scanning the README.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The server is open source and ready to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/MattJeff/orizn-mcp-server" rel="noopener noreferrer"&gt;github.com/MattJeff/orizn-mcp-server&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/orizn-visa-mcp" rel="noopener noreferrer"&gt;npmjs.com/package/orizn-visa-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it&lt;/strong&gt;: &lt;code&gt;npx orizn-visa-mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API docs&lt;/strong&gt;: &lt;a href="https://visa.orizn.app" rel="noopener noreferrer"&gt;visa.orizn.app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building an AI agent that touches travel, immigration, or international logistics, plug this in and your agent gets reliable visa data instead of hallucinated guesses. And if you're building your own MCP server -- steal the patterns. The protocol is simple, the SDK is solid, and the ecosystem is growing fast.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>tutorial</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
