<?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: Sam</title>
    <description>The latest articles on DEV Community by Sam (@rabbitholeinphp).</description>
    <link>https://dev.to/rabbitholeinphp</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%2F3830080%2F8b292e68-c3fa-4580-b8ed-c7f759dce728.png</url>
      <title>DEV Community: Sam</title>
      <link>https://dev.to/rabbitholeinphp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rabbitholeinphp"/>
    <language>en</language>
    <item>
      <title>MX Records Explained: How Email Actually Finds You</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Tue, 03 Nov 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/rabbitholeinphp/-mx-records-explained-how-email-actually-finds-you-2d9b</link>
      <guid>https://dev.to/rabbitholeinphp/-mx-records-explained-how-email-actually-finds-you-2d9b</guid>
      <description>&lt;p&gt;The plain-English guide to mail routing. What MX records are, the ports that matter, SMTP versus ESMTP, hosted versus self-hosted servers, transport security, and a ready-to-paste Claude prompt that builds your server config for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick answer, in case that is all you came for:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An MX record is a DNS entry that names the mail server responsible for accepting email for your domain, along with a priority number. When someone emails &lt;a href="mailto:you@yourcompany.com"&gt;you@yourcompany.com&lt;/a&gt;, the sending server looks up the MX records for yourcompany.com and connects to the host with the lowest priority number first. A typical record looks like &lt;code&gt;10 mx.yourprovider.com&lt;/code&gt;. Lower priority wins. No MX record usually means mail bounces. An MX record proves the domain can receive mail, but it does not prove a specific mailbox exists.&lt;/p&gt;

&lt;p&gt;MX is about inbound mail. SPF, DKIM and DMARC are about outbound mail. Those are four different records doing four different jobs, and mixing them up is why people spend afternoons fixing the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bounce that makes no sense
&lt;/h2&gt;

&lt;p&gt;A customer tells you they emailed and got no reply. You check. Nothing arrived.&lt;/p&gt;

&lt;p&gt;They forward you the bounce. It says something like &lt;code&gt;Recipient address rejected: Domain not found&lt;/code&gt;, or it just sat in a queue for four days and gave up.&lt;/p&gt;

&lt;p&gt;You check your website. It loads fine. Your domain clearly exists. So what happened?&lt;/p&gt;

&lt;p&gt;Your domain has an A record pointing at your website and no MX record pointing at your mail. To a browser, everything is perfect. To a mail server, your domain has nowhere to put a letter.&lt;/p&gt;

&lt;p&gt;This happens constantly during DNS migrations, after a hosting change, and on any domain where somebody set up the website and assumed email came along for free. It does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an MX record, in plain English?
&lt;/h2&gt;

&lt;p&gt;MX stands for Mail Exchanger. It is defined in RFC 1035, with its role in mail routing spelled out in RFC 5321.&lt;/p&gt;

&lt;p&gt;Think about a physical mailroom.&lt;/p&gt;

&lt;p&gt;Your domain name is the company name on the front of the building. The MX record is the sign in the lobby that says "all deliveries to the loading dock around the back, and if that dock is closed, use the side entrance."&lt;/p&gt;

&lt;p&gt;When a courier arrives with a package addressed to your company, they read that sign and go where it tells them. Without the sign, they stand in the lobby, then eventually take the package back to the depot.&lt;/p&gt;

&lt;p&gt;That is an MX record. It is the delivery instruction for everything addressed to the part after the @.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the priority number exists
&lt;/h2&gt;

&lt;p&gt;Every MX record has a number in front of the hostname:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10  mx1.provider.com
20  mx2.provider.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lower is preferred. The sender tries mx1 first because 10 is lower than 20. If mx1 does not answer, it falls back to mx2.&lt;/p&gt;

&lt;p&gt;The numbers themselves are meaningless in isolation. 10 and 20 are conventional, but 1 and 2 work identically, as do 0 and 5. All that matters is the ordering.&lt;/p&gt;

&lt;p&gt;Hosts sharing the same number are load balanced, used in random order. That is why Google Workspace tenants historically published five hosts at various priorities: redundancy plus spread.&lt;/p&gt;

&lt;h2&gt;
  
  
  How email actually gets delivered, step by step
&lt;/h2&gt;

&lt;p&gt;This is the whole journey, from your keyboard to someone else's inbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1.&lt;/strong&gt; You hit send. Your mail client (the MUA, more on that terminology below) connects to your outgoing mail server, usually on port 587, and hands over the message with your username and password.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2.&lt;/strong&gt; Your server takes ownership. It queues the message and reads the recipient address, say &lt;a href="mailto:sarah@example.com"&gt;sarah@example.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3.&lt;/strong&gt; It looks up the MX records for example.com. One DNS query. It gets back a list of hostnames with priority numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4.&lt;/strong&gt; It resolves the lowest priority host to an IP address. Another DNS query, this time for the A or AAAA record of that hostname.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5.&lt;/strong&gt; It opens an SMTP connection on port 25. Server to server delivery always uses port 25. There is no alternative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6.&lt;/strong&gt; The two servers negotiate. They greet each other, agree on extensions, and almost always upgrade the connection to encryption using STARTTLS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7.&lt;/strong&gt; The message transfers. Sender address, recipient address, then the message itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 8.&lt;/strong&gt; The receiving server runs its checks. SPF, DKIM, DMARC, reputation, content filtering, all of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 9.&lt;/strong&gt; It accepts or rejects. If it accepts, the message is handed to a delivery agent that files it in Sarah's mailbox. If it rejects, your server generates a bounce back to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 10.&lt;/strong&gt; Sarah reads it. Her mail client connects over IMAP and pulls it down.&lt;/p&gt;

&lt;p&gt;Every one of those steps has a name, a port, and a protocol. Here they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every term you need, decoded
&lt;/h2&gt;

&lt;p&gt;This is the reference section. Nothing here is complicated once somebody writes it in plain English.&lt;/p&gt;

&lt;h3&gt;
  
  
  The three agents
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Stands for&lt;/th&gt;
&lt;th&gt;What it actually is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MUA&lt;/td&gt;
&lt;td&gt;Mail User Agent&lt;/td&gt;
&lt;td&gt;The thing you look at. Outlook, Gmail's web interface, Apple Mail, Thunderbird.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTA&lt;/td&gt;
&lt;td&gt;Mail Transfer Agent&lt;/td&gt;
&lt;td&gt;The server software that moves mail between servers. Postfix, Exim, Exchange, Sendmail. This is the piece MX records point at.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MDA&lt;/td&gt;
&lt;td&gt;Mail Delivery Agent&lt;/td&gt;
&lt;td&gt;Takes the message from the MTA and files it in the right mailbox. Dovecot, Courier, procmail.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A single machine often runs all three. In modern setups they are frequently separate services or separate providers entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  The protocols
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SMTP&lt;/td&gt;
&lt;td&gt;Simple Mail Transfer Protocol. The language servers speak to hand mail to each other.&lt;/td&gt;
&lt;td&gt;Sending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESMTP&lt;/td&gt;
&lt;td&gt;Extended SMTP. The modern version, which added extensions like STARTTLS, authentication, size limits and 8-bit characters. When a server greets you with EHLO instead of HELO, you are speaking ESMTP. Almost everything today is ESMTP.&lt;/td&gt;
&lt;td&gt;Sending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IMAP&lt;/td&gt;
&lt;td&gt;Internet Message Access Protocol. Your client reads mail that stays on the server, synced across devices.&lt;/td&gt;
&lt;td&gt;Receiving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POP3&lt;/td&gt;
&lt;td&gt;Post Office Protocol v3. Your client downloads mail and typically deletes it from the server. Largely obsolete.&lt;/td&gt;
&lt;td&gt;Receiving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Submission&lt;/td&gt;
&lt;td&gt;The specific act of a mail client handing a message to its own server, as opposed to server-to-server transfer. Uses port 587 with authentication.&lt;/td&gt;
&lt;td&gt;Sending&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The distinction that trips people up: SMTP moves mail, IMAP and POP3 read mail. They are not alternatives to each other. You use SMTP to send and IMAP to receive, both, always.&lt;/p&gt;

&lt;h3&gt;
  
  
  The security layers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TLS&lt;/td&gt;
&lt;td&gt;Transport Layer Security&lt;/td&gt;
&lt;td&gt;Encrypts the connection. Replaced SSL, though people still say SSL out of habit.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STARTTLS&lt;/td&gt;
&lt;td&gt;Part of ESMTP&lt;/td&gt;
&lt;td&gt;Starts a connection unencrypted, then upgrades it to TLS. This is how nearly all server-to-server mail is encrypted. Its weakness is that an attacker in the middle can strip the upgrade offer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implicit TLS&lt;/td&gt;
&lt;td&gt;Port 465&lt;/td&gt;
&lt;td&gt;The connection is encrypted from the first byte. No upgrade step to strip.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTA-STS&lt;/td&gt;
&lt;td&gt;RFC 8461&lt;/td&gt;
&lt;td&gt;Lets your domain declare that senders must use validated TLS and must never fall back to plaintext. Closes the STARTTLS stripping hole.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS-RPT&lt;/td&gt;
&lt;td&gt;RFC 8460&lt;/td&gt;
&lt;td&gt;Asks sending providers to send you daily reports on TLS delivery failures. Pure reporting. Cannot break anything.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DANE / TLSA&lt;/td&gt;
&lt;td&gt;RFC 6698, RFC 7672&lt;/td&gt;
&lt;td&gt;Pins your mail server's certificate in DNS. Requires DNSSEC. Common in the Netherlands, Germany and public sector, rare elsewhere.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNSSEC&lt;/td&gt;
&lt;td&gt;RFC 4033 to 4035&lt;/td&gt;
&lt;td&gt;Cryptographically signs DNS answers so tampering is detectable. Without it, an attacker who can forge DNS can redirect your MX lookup entirely.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The reputation signals
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PTR / reverse DNS&lt;/td&gt;
&lt;td&gt;Maps an IP address back to a hostname, published by whoever owns the IP block.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCrDNS&lt;/td&gt;
&lt;td&gt;Forward-Confirmed reverse DNS. The PTR name resolves back to the same IP it came from. Receivers strongly prefer this. Missing or generic reverse DNS is one of the oldest spam signals there is, and a frequent reason self-hosted mail gets rejected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Greylisting&lt;/td&gt;
&lt;td&gt;The receiver temporarily rejects a first delivery attempt from an unknown sender. Real servers retry. Most spam software does not.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smarthost / relay&lt;/td&gt;
&lt;td&gt;An external SMTP server your machine hands all outbound mail to, instead of delivering it itself. How you send mail when port 25 is blocked.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catch-all&lt;/td&gt;
&lt;td&gt;A domain that accepts mail for every possible address. Makes mailbox verification impossible.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Null MX&lt;/td&gt;
&lt;td&gt;RFC 7505. A record of &lt;code&gt;0 .&lt;/code&gt; (priority zero, a single dot) declaring the domain accepts no mail at all, so senders fail instantly instead of retrying for days.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implicit MX&lt;/td&gt;
&lt;td&gt;RFC 5321 behaviour where a sender finding no MX record may fall back to the domain's A record. Unreliable. Many senders do not try.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Email ports: which one, and when
&lt;/h2&gt;

&lt;p&gt;This table answers more support tickets than any other single thing in this article.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Encryption&lt;/th&gt;
&lt;th&gt;What it is for&lt;/th&gt;
&lt;th&gt;Use it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;SMTP&lt;/td&gt;
&lt;td&gt;STARTTLS&lt;/td&gt;
&lt;td&gt;Server to server delivery. The only port MX delivery uses.&lt;/td&gt;
&lt;td&gt;Yes, for MTAs. Never for mail clients.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;587&lt;/td&gt;
&lt;td&gt;SMTP submission&lt;/td&gt;
&lt;td&gt;STARTTLS&lt;/td&gt;
&lt;td&gt;Mail client to your own server, with authentication.&lt;/td&gt;
&lt;td&gt;Yes. This is the default for sending from an app or client.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;465&lt;/td&gt;
&lt;td&gt;SMTPS&lt;/td&gt;
&lt;td&gt;Implicit TLS&lt;/td&gt;
&lt;td&gt;Client submission, encrypted from the first byte. Deprecated in 1998, then formally reinstated in 2018.&lt;/td&gt;
&lt;td&gt;Yes, fine. Slightly safer than 587 in hostile networks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2525&lt;/td&gt;
&lt;td&gt;SMTP&lt;/td&gt;
&lt;td&gt;STARTTLS&lt;/td&gt;
&lt;td&gt;Not a standard. An unofficial alternative many providers offer when 587 is blocked.&lt;/td&gt;
&lt;td&gt;Only as a fallback.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;143&lt;/td&gt;
&lt;td&gt;IMAP&lt;/td&gt;
&lt;td&gt;STARTTLS&lt;/td&gt;
&lt;td&gt;Reading mail.&lt;/td&gt;
&lt;td&gt;Prefer 993.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;993&lt;/td&gt;
&lt;td&gt;IMAPS&lt;/td&gt;
&lt;td&gt;Implicit TLS&lt;/td&gt;
&lt;td&gt;Reading mail, encrypted.&lt;/td&gt;
&lt;td&gt;Yes. Default for IMAP.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;td&gt;POP3&lt;/td&gt;
&lt;td&gt;STARTTLS&lt;/td&gt;
&lt;td&gt;Downloading mail.&lt;/td&gt;
&lt;td&gt;Only if you have a reason.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;995&lt;/td&gt;
&lt;td&gt;POP3S&lt;/td&gt;
&lt;td&gt;Implicit TLS&lt;/td&gt;
&lt;td&gt;Downloading mail, encrypted.&lt;/td&gt;
&lt;td&gt;Only if you have a reason.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule to remember: &lt;strong&gt;port 25 is for servers talking to servers. Port 587 is for you talking to your server.&lt;/strong&gt; If a setup guide tells you to configure your email app on port 25, that guide is at least fifteen years out of date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Port 25 and your cloud provider: the thing nobody warns you about
&lt;/h2&gt;

&lt;p&gt;If you plan to run your own mail server, read this before you pick a host.&lt;/p&gt;

&lt;p&gt;Almost every cloud provider blocks outbound port 25 by default. They do it because freshly created VPS instances are a favourite spam vector: a spammer spins one up, blasts a million messages, and abandons it. Blocking the port at the network layer kills that business model.&lt;/p&gt;

&lt;p&gt;The consequence for you is that your mail server can receive mail perfectly and cannot send a single message. Your logs fill with connection timeouts and nothing explains why.&lt;/p&gt;

&lt;p&gt;Here is roughly where the major providers sit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Outbound port 25&lt;/th&gt;
&lt;th&gt;Getting it opened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OVHcloud&lt;/td&gt;
&lt;td&gt;Blocked by default at the network level&lt;/td&gt;
&lt;td&gt;Unblock available through the Manager or a support request. Widely reported as granted on first request for accounts in good standing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hetzner&lt;/td&gt;
&lt;td&gt;Blocked on new accounts&lt;/td&gt;
&lt;td&gt;Opened on request, generally straightforward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DigitalOcean, Vultr&lt;/td&gt;
&lt;td&gt;Blocked&lt;/td&gt;
&lt;td&gt;Support ticket, usually granted with a stated use case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS EC2, Google Cloud&lt;/td&gt;
&lt;td&gt;Blocked&lt;/td&gt;
&lt;td&gt;Effectively a dead end. AWS largely stopped granting these years ago&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A note on OVH specifically, since it comes up constantly in self-hosting discussions. OVH is one of the more permissive mainstream hosts here, and its unblock path is documented rather than discretionary. But "permissive" is not the same as "open by default." OVHcloud's own documentation states that outgoing port 25 is blocked by default across dedicated servers, VPS and Public Cloud instances to prevent abuse, and directs you to use port 587 or request an unblock.&lt;/p&gt;

&lt;p&gt;Two things worth knowing before you rely on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The unblock is revocable. OVH runs anti-spam monitoring on its network. If mail from your IP gets flagged, port 25 is re-blocked, and repeated incidents extend the cooldown or make it permanent.&lt;/li&gt;
&lt;li&gt;Behaviour can vary by data centre and account age. Test it the moment you provision, not the day before launch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the honest advice, whatever your host: just because you can deliver directly on port 25 does not mean you should. A brand new IP address has no sending reputation, and building one takes months of consistent, low-complaint volume. For most people the right architecture is to run your own server for receiving and for control, and relay outbound mail through an established provider as a smarthost. You keep ownership of your data and skip the reputation problem entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually runs the world's email
&lt;/h2&gt;

&lt;p&gt;There is a useful distinction here between who hosts the mailboxes and what software runs the server. Both matter, for different reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hosted providers
&lt;/h3&gt;

&lt;p&gt;As of August 2026, the picture is dominated by exactly who you would guess. Google Workspace sits at 17.4% of surveyed sites and Microsoft 365 at 13.5%. Together that is roughly 31% of domains. The concentration is far higher among large organisations.&lt;/p&gt;

&lt;p&gt;The middle of that list is the interesting part. Hostinger, IONOS, GoDaddy, Namecheap, Zoho and OVH together represent an enormous number of small business domains where nobody has ever looked at the authentication setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the provider from the MX hostname
&lt;/h3&gt;

&lt;p&gt;This is a genuinely useful skill. MX hostnames are a fingerprint.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;MX hostname pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Workspace&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;smtp.google.com&lt;/code&gt;, or the older &lt;code&gt;aspmx.l.google.com&lt;/code&gt; set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft 365&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;tenant&amp;gt;.mail.protection.outlook.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zoho Mail&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mx.zoho.com&lt;/code&gt;, &lt;code&gt;mx2.zoho.com&lt;/code&gt;, plus regional variants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proton Mail&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mail.protonmail.ch&lt;/code&gt;, &lt;code&gt;mailsec.protonmail.ch&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fastmail&lt;/td&gt;
&lt;td&gt;&lt;code&gt;in1-smtp.messagingengine.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon WorkMail / SES&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inbound-smtp.&amp;lt;region&amp;gt;.amazonaws.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mimecast&lt;/td&gt;
&lt;td&gt;hostnames ending &lt;code&gt;mimecast.com&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proofpoint&lt;/td&gt;
&lt;td&gt;hostnames ending &lt;code&gt;pphosted.com&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Barracuda&lt;/td&gt;
&lt;td&gt;hostnames ending &lt;code&gt;barracudanetworks.com&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registrar or shared hosting&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mx1.registrar-servers.com&lt;/code&gt;, &lt;code&gt;...secureserver.net&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last three security vendors are worth calling out. Mimecast, Proofpoint and Barracuda are gateways. They sit in front of the real mailbox provider and filter everything. If you see one in an MX record, the actual mailbox host is hidden behind it, and the organisation is almost certainly an enterprise with a security budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  The open source mail servers
&lt;/h3&gt;

&lt;p&gt;If you self-host, this is the software that does the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The established MTAs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Postfix.&lt;/strong&gt; Written by Wietse Venema in 1997 as a secure alternative to Sendmail. Modular, well documented, sane defaults. The default choice for most new deployments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exim.&lt;/strong&gt; Written at the University of Cambridge in 1995. Enormously flexible with a configuration language that can do almost anything. The default MTA on Debian and the standard in the cPanel shared-hosting world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sendmail.&lt;/strong&gt; The original, dating to 1983. Still running in places. Its configuration format is legendary in the way that a difficult mountain is legendary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;qmail.&lt;/strong&gt; Dan Bernstein's 1997 security-focused MTA. Development stalled for years. notqmail is the community continuation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenSMTPD.&lt;/strong&gt; From the OpenBSD project. Deliberately minimal configuration, security-first design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The modern generation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stalwart.&lt;/strong&gt; Combines MTA, delivery agent and IMAP access in one binary. Written in Rust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mox.&lt;/strong&gt; All-in-one server with automatic TLS via Let's Encrypt, built-in DMARC analysis and webmail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chasquid.&lt;/strong&gt; Lightweight, simple, with enforced TLS as a design principle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KumoMTA.&lt;/strong&gt; Built for high-volume outbound sending rather than general mailbox hosting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Haraka.&lt;/strong&gt; Node.js based, plugin-driven, popular where you need custom logic in the mail path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The all-in-one stacks:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mailcow, iRedMail, Mailu, Mail-in-a-Box, Modoboa, docker-mailserver, Poste.io.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosted versus self-hosted, honestly
&lt;/h3&gt;

&lt;p&gt;I am going to be blunt, because a lot of writing on this topic is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The software is free. The operation is not.&lt;/strong&gt; You are signing up for TLS certificate renewal, spam filtering that keeps working, blocklist monitoring, backups you have actually tested, security patching on an internet-facing service, and reputation management on an IP address that starts with none. The realistic cost is not the server, it is engineering hours forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-host when:&lt;/strong&gt; you have a specific data residency or sovereignty requirement, you want to actually understand the stack, you are running a lab, or you have compliance rules that make third-party hosting difficult.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a provider when:&lt;/strong&gt; email needs to work reliably and nobody on your team wants to be paged about it. Which is most people, most of the time. There is no shame in this.&lt;/p&gt;

&lt;p&gt;The middle path is underrated: self-host for receiving and mailbox control, relay outbound through a reputable smarthost. You get the control without inheriting the deliverability problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  MX versus SPF versus DKIM versus DMARC
&lt;/h2&gt;

&lt;p&gt;These four get confused constantly, and fixing the wrong record is the usual outcome.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Answers&lt;/th&gt;
&lt;th&gt;Published at&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MX&lt;/td&gt;
&lt;td&gt;Inbound&lt;/td&gt;
&lt;td&gt;Which server accepts mail for this domain, and in what order&lt;/td&gt;
&lt;td&gt;example.com, type MX&lt;/td&gt;
&lt;td&gt;RFC 1035, RFC 5321&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPF&lt;/td&gt;
&lt;td&gt;Outbound&lt;/td&gt;
&lt;td&gt;Which servers may send mail using this domain&lt;/td&gt;
&lt;td&gt;example.com, TXT starting &lt;code&gt;v=spf1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;RFC 7208&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DKIM&lt;/td&gt;
&lt;td&gt;Outbound&lt;/td&gt;
&lt;td&gt;Was this message signed by a key the domain published, and unaltered since&lt;/td&gt;
&lt;td&gt;&lt;code&gt;selector._domainkey.example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RFC 6376&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DMARC&lt;/td&gt;
&lt;td&gt;Outbound&lt;/td&gt;
&lt;td&gt;What receivers should do when SPF or DKIM alignment fails, and where to report&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_dmarc.example.com&lt;/code&gt;, TXT&lt;/td&gt;
&lt;td&gt;RFC 7489&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A domain can receive mail perfectly and still fail every outbound authentication check. These are separate systems. Getting your MX right does nothing for your deliverability when sending, and vice versa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full tutorial: publishing MX records
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Decide where mail should land
&lt;/h3&gt;

&lt;p&gt;Pick your provider first. Changing MX records later is easy, but migrating mailboxes is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Get the values
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Google Workspace&lt;/strong&gt;, newer tenants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1   smtp.google.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Older tenants using the five ASPMX hosts do not need to migrate. Both are supported. Do not mix the two sets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microsoft 365:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0   yourdomain-com.mail.protection.outlook.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One host per tenant. The label is your tenant's onmicrosoft.com prefix with dots replaced by hyphens. Microsoft publishes the exact value in the admin centre. Never guess it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zoho Mail:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10  mx.zoho.com
20  mx2.zoho.com
50  mx3.zoho.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regional suffixes such as &lt;code&gt;.eu&lt;/code&gt; and &lt;code&gt;.in&lt;/code&gt; exist. Use the ones your Zoho console shows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your own server:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10  mail.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create an A record for &lt;code&gt;mail.yourdomain.com&lt;/code&gt; pointing at your server's IP, and set the PTR record for that IP to &lt;code&gt;mail.yourdomain.com&lt;/code&gt; in your host's control panel. All three must agree. This is forward-confirmed reverse DNS, and skipping it is the most common reason self-hosted mail gets rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A domain that sends and receives nothing:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MX     0 .
TXT    v=spf1 -all
TXT    _dmarc    v=DMARC1; p=reject; sp=reject; rua=mailto:dmarc@yourdomain.com; fo=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;0 .&lt;/code&gt; is a null MX. It tells senders to fail immediately and cleanly rather than retrying for four days. Combined with an empty SPF and a reject policy, it closes a domain attackers would otherwise like precisely because nobody is watching it.&lt;/p&gt;

&lt;p&gt;Publish this on every parked domain, defensive registration and web-only hostname you own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Add them in DNS
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What to enter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;MX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Name / Host&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@&lt;/code&gt; or blank, meaning the root domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority&lt;/td&gt;
&lt;td&gt;The number, entered in its own field on most panels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value / Points to&lt;/td&gt;
&lt;td&gt;The mail server hostname, usually with a trailing dot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL&lt;/td&gt;
&lt;td&gt;3600 normally. Lower it to 300 before a planned migration.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things that break setups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never point an MX record at an IP address.&lt;/strong&gt; It must be a hostname. Point the hostname at the IP with a separate A record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never point an MX at a CNAME.&lt;/strong&gt; It is explicitly not allowed and some senders will refuse it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove the old records when you migrate.&lt;/strong&gt; Leaving the previous provider's MX in place at a higher priority means some mail keeps landing in a mailbox nobody reads.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 4: Add transport security, in this order
&lt;/h3&gt;

&lt;p&gt;Do these one at a time. The order matters because the first one cannot break anything and the last one can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4a: TLS-RPT.&lt;/strong&gt; Reporting only. Completely safe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TXT   _smtp._tls   v=TLSRPTv1; rua=mailto:tlsrpt@yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now get daily reports on TLS delivery to your domain, including expired certificates and stripped connections. This is how you find out about problems before your users do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4b: MTA-STS in testing mode.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two parts. The DNS record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TXT   _mta-sts   v=STSv1; id=20260817T000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a policy file served over HTTPS with a valid certificate at &lt;code&gt;https://mta-sts.yourdomain.com/.well-known/mta-sts.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STSv1&lt;/span&gt;
&lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testing&lt;/span&gt;
&lt;span class="na"&gt;mx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smtp.google.com&lt;/span&gt;
&lt;span class="na"&gt;max_age&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;604800&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In testing mode, failures are reported through TLS-RPT rather than enforced. Nothing breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4c: Enforce.&lt;/strong&gt; Change one word and update the id:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mode: enforce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things that catch people out. Every MX host you accept mail on must be listed in the policy file, or mail to the missing one fails. And you must change the &lt;code&gt;id&lt;/code&gt; value whenever you edit the file, otherwise senders keep serving the cached policy until &lt;code&gt;max_age&lt;/code&gt; expires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4d: DANE, only if your zone is DNSSEC-signed.&lt;/strong&gt; Publish a TLSA record at &lt;code&gt;_25._tcp.&amp;lt;your MX host&amp;gt;&lt;/code&gt;. DANE is meaningful only with DNSSEC, because the certificate pin is only trustworthy if the DNS answer itself is signed. It is widely deployed in the Netherlands, Germany and parts of the public sector, and rare elsewhere. MTA-STS is the more common route. Either beats neither.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Test everything at once
&lt;/h3&gt;

&lt;p&gt;Use the free MX Record Checker from &lt;a href="https://www.emailverifyerapi.com/mx-record-checker" rel="noopener noreferrer"&gt;EmailVerifyerAPI&lt;/a&gt;. It runs nine records in one query and explains each result in plain English.&lt;/p&gt;

&lt;p&gt;One lookup returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MX hosts with priorities and TTLs&lt;/li&gt;
&lt;li&gt;The mail provider, matched from the hostname patterns&lt;/li&gt;
&lt;li&gt;SPF and DMARC, so you see the outbound half in the same view&lt;/li&gt;
&lt;li&gt;MTA-STS, TLS-RPT and DANE, the transport security layer almost no free checker shows&lt;/li&gt;
&lt;li&gt;Reverse DNS, resolving the primary MX to its IP and reporting the PTR&lt;/li&gt;
&lt;li&gt;DNSSEC, reported through the authenticated-data flag on the MX answer&lt;/li&gt;
&lt;li&gt;A verdict: configured, fallback only, or nothing at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No account, nothing stored, and only the domain is sent to a public resolver.&lt;/p&gt;

&lt;p&gt;What a real result looks like. Run &lt;code&gt;github.com&lt;/code&gt; through it and you get one MX record at priority 0 pointing at &lt;code&gt;github-com.mail.protection.outlook.com&lt;/code&gt;, immediately identifying Microsoft 365. SPF is published with a long list of includes covering Outlook, Google netblocks, Zendesk, Salesforce, Mailchimp, Marketo and SendGrid. DMARC is at &lt;code&gt;p=quarantine&lt;/code&gt; with &lt;code&gt;sp=reject&lt;/code&gt; and reporting enabled. MTA-STS, TLS-RPT and DANE are all absent. Reverse DNS resolves correctly. DNSSEC is unsigned.&lt;/p&gt;

&lt;p&gt;That is a well-run domain that has done the authentication work and not yet done the transport work, which describes the large majority of the internet. Absence of MTA-STS and DANE is graded as amber rather than red for exactly that reason: it is an available hardening step, not a fault.&lt;/p&gt;

&lt;p&gt;One documented limitation: MTA-STS has a DNS half and an HTTPS half. A browser-based tool can report the DNS half but cannot fetch the policy file from another origin. A domain with the TXT record but no reachable policy file is not actually enforcing anything, so verify the file loads yourself.&lt;/p&gt;

&lt;p&gt;Pair it with the dedicated SPF, DKIM and DMARC checkers when you need to go deep on one record.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Send a real test
&lt;/h3&gt;

&lt;p&gt;Send a message to an address on the domain. Then send one from it. Both directions, because they exercise completely different records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MX matters for your email list
&lt;/h2&gt;

&lt;p&gt;There is a commercial angle here that is easy to miss.&lt;/p&gt;

&lt;p&gt;Roughly 22% of marketing email addresses go invalid every year as people change jobs and companies fold, according to HubSpot's research. A domain-level MX lookup catches an entire category of those failures in a single query: dead domains, parked domains, and domains never configured for mail at all.&lt;/p&gt;

&lt;p&gt;It is the cheapest possible bounce check. It costs one DNS query and it runs in milliseconds.&lt;/p&gt;

&lt;p&gt;What it cannot tell you is whether a specific mailbox exists. Only an SMTP handshake can attempt that, and catch-all domains accept every address anyway, so even then the answer can be inconclusive. MX proves the door exists. It says nothing about who lives there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven mistakes I see most often
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No MX record on a domain that receives mail.&lt;/strong&gt; The A record fallback is unreliable and many senders never try it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MX pointing at an IP address.&lt;/strong&gt; Not allowed. Use a hostname with its own A record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MX pointing at a CNAME.&lt;/strong&gt; Also not allowed, and some senders will hard fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old provider's MX left in place after a migration.&lt;/strong&gt; Mail quietly splits between two mailboxes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No PTR record on a self-hosted sender.&lt;/strong&gt; One of the oldest spam signals in existence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing on parked domains.&lt;/strong&gt; Publish the null MX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confusing MX with SPF.&lt;/strong&gt; Adding your sending platform to your MX records does absolutely nothing. MX is inbound only.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A ready-to-paste Claude prompt for building your mail server
&lt;/h2&gt;

&lt;p&gt;If you are going to self-host, this is where an LLM genuinely earns its keep. Mail server configuration is a large amount of interlocking, well-documented, unforgiving detail, which is exactly the shape of problem Claude is good at.&lt;/p&gt;

&lt;p&gt;Copy the prompt below, fill in the bracketed values, and paste it into Claude. Ask follow-up questions as you work through it rather than trying to execute the whole thing blind.&lt;/p&gt;

&lt;p&gt;You are an experienced mail server administrator. I want to deploy a&lt;br&gt;
production-grade mail server and I need a complete, ordered configuration&lt;br&gt;
plan I can follow step by step.&lt;/p&gt;

&lt;p&gt;MY SETUP&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain: [yourdomain.com]&lt;/li&gt;
&lt;li&gt;Hosting provider: [OVHcloud / Hetzner / DigitalOcean / other]&lt;/li&gt;
&lt;li&gt;Server type and specs: [VPS, 4 vCPU, 8 GB RAM, Ubuntu 24.04]&lt;/li&gt;
&lt;li&gt;Static IPv4: [x.x.x.x]  IPv6: [yes / no]&lt;/li&gt;
&lt;li&gt;Expected volume: [e.g. 200 messages/day outbound, 20 mailboxes]&lt;/li&gt;
&lt;li&gt;Outbound port 25 status: [open / blocked / unblock requested]&lt;/li&gt;
&lt;li&gt;Preferred stack: [Postfix + Dovecot / Mailcow / Stalwart / recommend one]&lt;/li&gt;
&lt;li&gt;Outbound delivery: [direct on port 25 / relay through a smarthost]&lt;/li&gt;
&lt;li&gt;My Linux comfort level: [beginner / intermediate / advanced]&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WHAT I NEED FROM YOU&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;STACK RECOMMENDATION&lt;br&gt;
Given my volume, comfort level and provider, recommend a stack and&lt;br&gt;
explain the tradeoff in two or three sentences. If self-hosting is a&lt;br&gt;
bad idea for my situation, say so plainly and tell me why.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PRE-FLIGHT CHECKS&lt;br&gt;
Exact commands to verify before I install anything: is outbound port 25&lt;br&gt;
actually open, is my IP on any major blocklist, can I set a PTR record&lt;br&gt;
with this provider, is my IP range already burned.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DNS RECORDS&lt;br&gt;
Give me every record as a table I can work through: MX with priority,&lt;br&gt;
the A record for the mail hostname, PTR, SPF, DKIM, DMARC starting at&lt;br&gt;
p=none, TLS-RPT, and MTA-STS in testing mode. Show the literal values&lt;br&gt;
with my domain substituted in. Flag which ones I must get from the&lt;br&gt;
software after installation rather than write in advance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;INSTALLATION&lt;br&gt;
Ordered commands for my exact OS version. After each block, tell me&lt;br&gt;
what to check to confirm it worked before moving on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TLS&lt;br&gt;
Certificate issuance and automatic renewal. Which ports listen with&lt;br&gt;
implicit TLS and which use STARTTLS, and why.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SECURITY BASELINE&lt;br&gt;
Firewall rules with the specific ports. SASL authentication so I am&lt;br&gt;
not an open relay, and the exact command to verify I am not. Rate&lt;br&gt;
limiting. fail2ban. Disable anything I do not need.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ANTI-SPAM&lt;br&gt;
Inbound filtering (rspamd or SpamAssassin), and the outbound hygiene&lt;br&gt;
that keeps my IP clean.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DELIVERABILITY WARM-UP&lt;br&gt;
A realistic schedule for a brand new IP with no sending history, and&lt;br&gt;
the specific signals that tell me it is going badly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;VERIFICATION CHECKLIST&lt;br&gt;
Every test to run before I trust this with real mail: authentication&lt;br&gt;
headers on a delivered message, open relay test, TLS check, blocklist&lt;br&gt;
check, MX and reverse DNS confirmation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;OPERATIONS&lt;br&gt;
What to monitor, what to back up, what breaks silently, and what to&lt;br&gt;
check monthly. Include the specific log lines that indicate trouble.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;RULES&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask me clarifying questions before generating the plan if any of my
answers above are ambiguous or contradictory.&lt;/li&gt;
&lt;li&gt;Explain the reasoning behind each significant choice in one line. I want
to understand the system, not just paste commands.&lt;/li&gt;
&lt;li&gt;Flag every step that is destructive or hard to reverse.&lt;/li&gt;
&lt;li&gt;Where a hosted service would be the better engineering decision, say so.&lt;/li&gt;
&lt;li&gt;Assume I will get something wrong, and tell me how I will know.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two caveats worth stating. Verify every command against the official documentation for your distribution and your software version before running it on a production box, because syntax drifts between releases. And check the output against your provider's own docs on port 25 and PTR records, since those policies change and are provider-specific.&lt;/p&gt;

&lt;p&gt;Used well, this turns a multi-day research project into an afternoon. Used carelessly, it turns into an open relay. The difference is whether you read what you are pasting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is an MX record?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A DNS record naming the mail server responsible for accepting email for a domain, along with a priority number. Sending servers look up the MX records for the part after the @ and connect to the host with the lowest priority value first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does the MX priority number mean?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It sets the order senders try the listed hosts. Lower numbers are tried first, so priority 10 is preferred over 20. Hosts sharing the same value are used in random order to spread load. Higher-numbered hosts act as backups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if a domain has no MX record?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RFC 5321 allows a sender to fall back to the domain's A or AAAA record, known as the implicit MX rule. In practice this is unreliable, since many providers do not attempt it, so mail is likely delayed or rejected. A domain with neither MX nor A records cannot receive mail at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a valid MX record mean the email address exists?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. It only proves the domain has somewhere to deliver. Confirming a specific mailbox requires an SMTP handshake, and catch-all domains accept every address, so even that can be inconclusive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between port 25 and port 587?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Port 25 is for server-to-server delivery between mail transfer agents. Port 587 is for a mail client submitting a message to its own server, with authentication. Configure your apps and clients on 587. Never on 25.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between SMTP and ESMTP?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ESMTP is the extended, modern version of SMTP. It added STARTTLS encryption, authentication, message size limits and 8-bit character support. A server greeting with EHLO rather than HELO is speaking ESMTP, which today is essentially all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between IMAP and POP3?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;IMAP leaves mail on the server and syncs it across devices. POP3 downloads mail to one device and typically deletes the server copy. Use IMAP unless you have a specific reason not to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is my cloud provider blocking port 25?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To prevent spam from compromised or disposable instances, which are a major source of abuse. Most providers will unblock it on request with a stated legitimate use case. AWS and Google Cloud are the notable exceptions where it is effectively permanent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use OVH for a mail server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, and it is one of the friendlier mainstream hosts for it. Outbound port 25 is blocked by default at the network level, but OVH provides a documented unblock path through the Manager or a support request, generally granted for accounts in good standing. Be aware the unblock is revocable if the IP is later flagged for spam.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is MTA-STS and do I need it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MTA-STS lets your domain declare that senders must use validated TLS and must never fall back to plaintext, closing the hole where an attacker strips the STARTTLS offer. It is not required, but it is a meaningful hardening step. Deploy TLS-RPT first, then MTA-STS in testing mode, then enforce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a null MX record?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A record of &lt;code&gt;0 .&lt;/code&gt; declaring that a domain accepts no mail. Senders fail immediately and cleanly instead of retrying for days. Publish it on every parked and brand-defensive domain you own, alongside &lt;code&gt;v=spf1 -all&lt;/code&gt; and a DMARC reject policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can an MX record point to an IP address or a CNAME?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No to both. An MX record must name a hostname, and that hostname must resolve through an A or AAAA record, not a CNAME. Pointing an MX at a CNAME is explicitly disallowed and some senders will reject it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which open source mail server should I use?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Postfix paired with Dovecot is the conventional answer and the best documented. Mailcow or iRedMail if you want a full stack with a control panel rather than assembling one. Stalwart or Mox if you want a modern single-binary approach. Exim if you are already in the cPanel world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do my MX changes not show up yet?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DNS answers are cached for the length of each record's TTL. Until it elapses, resolvers may keep serving the old answer. Lower the TTL before a planned migration and raise it afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your action plan
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Right now, 5 minutes:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run your domain through the free MX checker.&lt;/li&gt;
&lt;li&gt;Confirm you have MX records, that they point at hostnames rather than IPs, and that no old provider is still listed.&lt;/li&gt;
&lt;li&gt;Note whether SPF and DMARC came back found.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;This week, 30 minutes:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Publish the null MX set on every parked domain you own.&lt;/li&gt;
&lt;li&gt;Add TLS-RPT. It is one record and it cannot break anything.&lt;/li&gt;
&lt;li&gt;If you self-host, confirm your PTR record resolves forward and back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;This month:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy MTA-STS in testing mode, read the TLS-RPT reports, then enforce.&lt;/li&gt;
&lt;li&gt;Finish the outbound side: SPF, then DKIM, then DMARC to enforcement.&lt;/li&gt;
&lt;li&gt;Set a quarterly reminder to re-run all four checkers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MX is the record that decides whether mail reaches you at all. Everything else in email authentication decides whether your mail reaches other people. You need both, and they are entirely separate jobs.&lt;/p&gt;

&lt;p&gt;If you only do one thing today, run your domain through a checker and see what nine records say about a setup you probably have not looked at in years.&lt;/p&gt;

</description>
      <category>web</category>
      <category>devops</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cybersecurity in 2026: What 2025 Broke and What You Need to Do About It</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Wed, 28 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/rabbitholeinphp/cybersecurity-in-2026-what-2025-broke-and-what-you-need-to-do-about-it-12d9</link>
      <guid>https://dev.to/rabbitholeinphp/cybersecurity-in-2026-what-2025-broke-and-what-you-need-to-do-about-it-12d9</guid>
      <description>&lt;p&gt;Let me give you the short version first.&lt;/p&gt;

&lt;p&gt;2025 was the year attackers stopped breaking down doors and started walking in with stolen keys. 2026 is the year they handed those keys to an AI agent. And 2027 is the year the cryptography under all of it starts to change for real, driven by post-quantum standards like ML-KEM.&lt;/p&gt;

&lt;p&gt;If you only do three things this year: kill reused passwords, inventory your encryption, and treat every AI agent in your stack as a privileged user.&lt;/p&gt;

&lt;p&gt;I have been writing about growth and technology for a long time. I have never seen a security cycle move this fast. So I am going to walk you through it the way I would explain it to a friend over coffee. No jargon walls. No fear selling. Just what happened, what it means for you, and what to do about it.&lt;/p&gt;

&lt;p&gt;Let's get into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Changed in 2025
&lt;/h2&gt;

&lt;p&gt;For years, the story of a breach was simple. Someone clicked a bad link, malware landed, and defenders hunted the malware. In 2025 that story fell apart.&lt;/p&gt;

&lt;p&gt;Credentials and tokens became the main event.&lt;/p&gt;

&lt;p&gt;The clearest example was the Salesloft Drift incident. Attackers tracked as UNC6395 compromised the Drift chatbot integration and stole the OAuth tokens that connected it to customer Salesforce environments. Between August 8 and 18, 2025, they used those tokens to query and export data from hundreds of organizations. The tokens were trusted, so nothing looked wrong.&lt;/p&gt;

&lt;p&gt;Coverage from SOCRadar lists an affected roster that reads like a who's who of the security industry itself: Cloudflare, Zscaler, Palo Alto Networks, Tenable, CyberArk, Proofpoint, and many more.&lt;/p&gt;

&lt;p&gt;Read that again. Security vendors got hit through a chatbot integration. Not because their firewalls failed, but because a trusted connector held a long-lived token.&lt;/p&gt;

&lt;p&gt;Supply chains became the fastest way in. The Shai-Hulud campaign turned npm into a self-propagating attack surface. Unit 42 documented the second wave, which executed during pre-install rather than post-install and exposed secrets across more than 25,000 repositories tied to roughly 350 users. IBM's X-Force team later reported that major supply chain and third-party breach incidents quadrupled over five years.&lt;/p&gt;

&lt;p&gt;And then AI stopped being a helper and became an operator.&lt;/p&gt;

&lt;p&gt;In November 2025, Anthropic disclosed that it had detected and disrupted a campaign it attributed to a Chinese state-sponsored group tracked as GTG-1002. The group used Claude Code, orchestrated through the Model Context Protocol, to run reconnaissance, vulnerability discovery, exploitation, credential harvesting, lateral movement, and exfiltration against roughly 30 high-value targets.&lt;/p&gt;

&lt;p&gt;The detail that matters: the AI executed an estimated 80 to 90 percent of the tactical work, with humans mostly approving phase transitions. The threat actor got past model safeguards by convincing the system it was doing authorized defensive testing.&lt;/p&gt;

&lt;p&gt;That is the moment the industry now points to as the dividing line.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 2026 Looks Like Right Now
&lt;/h2&gt;

&lt;p&gt;Here is the current picture, backed by numbers rather than vibes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phishing is not dying. It is specializing.&lt;/strong&gt; Microsoft telemetry cited by Convergence Networks shows roughly 7.6 billion phishing threats detected in a single three-month window this year, with credential phishing accounting for 94 to 96 percent of malicious payload attacks each month in the second quarter of 2026. Traditional malware made up only 4 to 6 percent. QR code phishing peaked at 18.7 million attacks in March 2026.&lt;/p&gt;

&lt;p&gt;Attackers do not want to infect your laptop anymore. They want your login, because a valid login does not trigger anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI risk moved to the top of the executive worry list.&lt;/strong&gt; The World Economic Forum's Global Cybersecurity Outlook 2026 found that 87 percent of respondents identified AI-related vulnerabilities as the fastest-growing cyber risk. The FBI's IC3 recorded more than 22,000 AI-related complaints and over $893 million in adjusted losses for 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defenders are still slow where it counts.&lt;/strong&gt; The 2026 Verizon Data Breach Investigations Report found a median of 43 days to fully resolve known vulnerabilities, with only 26 percent of CISA-listed critical vulnerabilities fully remediated across the 13,000 organizations surveyed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational technology is being mapped, not just probed.&lt;/strong&gt; Dragos now tracks 26 OT threat groups. Its year in review describes adversaries actively mapping control loops to manipulate physical processes with precision, including wiper deployments against Ukrainian ISPs and Polish combined heat and power facilities in December 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the internet's cryptography is half-migrated.&lt;/strong&gt; A 2026 measurement study of post-quantum readiness found that 50.7 percent of analyzed domains remain fully reliant on classical key exchange, while 49.3 percent already negotiate hybrid post-quantum key exchange. That second number would have been near zero three years ago.&lt;/p&gt;

&lt;p&gt;Which brings us to the part everyone finds intimidating and nobody should.&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-Quantum Cryptography in Plain English
&lt;/h2&gt;

&lt;p&gt;Let me strip this down.&lt;/p&gt;

&lt;p&gt;Most of the encryption protecting your traffic today does two different jobs with two different kinds of math.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symmetric encryption (AES-256)&lt;/strong&gt; scrambles the actual data. A quantum computer weakens it somewhat, but doubling key length handles that. AES-256 is fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public-key cryptography (RSA, elliptic curve)&lt;/strong&gt; handles key exchange and signatures. This is the part that lets your browser agree on a secret with a server it has never met. A sufficiently large quantum computer running Shor's algorithm breaks it completely.&lt;/p&gt;

&lt;p&gt;So the migration is not about replacing all encryption. It is about replacing the handshake and the signatures.&lt;/p&gt;

&lt;h3&gt;
  
  
  What ML-KEM Actually Is
&lt;/h3&gt;

&lt;p&gt;ML-KEM stands for Module-Lattice-Based Key-Encapsulation Mechanism. It was standardized by NIST as FIPS 203 in August 2024, based on the CRYSTALS-Kyber submission. Two companions shipped alongside it: ML-DSA (FIPS 204, signatures, from Dilithium) and SLH-DSA (FIPS 205, hash-based signatures, from SPHINCS+).&lt;/p&gt;

&lt;p&gt;A KEM is simpler than it sounds. Instead of encrypting a secret and sending it, the sender generates a shared secret and a ciphertext at the same time, sends the ciphertext, and the receiver uses their private key to recover the same shared secret. Same outcome as a key exchange, different mechanics, and the underlying math (module lattices) has no known efficient quantum attack.&lt;/p&gt;

&lt;p&gt;ML-KEM comes in three sizes: 512, 768, and 1024. Higher numbers mean bigger keys and higher security levels. ML-KEM-768 has become the practical default on the open internet. ML-KEM-1024 targets NIST Security Level 5, which is what you use when the data has to stay secret for decades.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Everything Is Hybrid
&lt;/h3&gt;

&lt;p&gt;Nearly every real deployment pairs a classical algorithm with a post-quantum one. You will see X25519 + ML-KEM-768 in TLS, or X448 + ML-KEM-1024 in higher-assurance systems. Both key exchanges run, and the results are combined. If the lattice math turns out to have a flaw, the classical half still protects you. If a quantum computer arrives, the post-quantum half still protects you.&lt;/p&gt;

&lt;p&gt;Research on hybrid key exchange has consistently shown minimal performance overhead, which is why adoption moved as fast as it did.&lt;/p&gt;

&lt;h3&gt;
  
  
  Harvest Now, Decrypt Later
&lt;/h3&gt;

&lt;p&gt;This is the reason you cannot wait for Q-Day to act.&lt;/p&gt;

&lt;p&gt;An adversary records your encrypted traffic today. They cannot read it today. They store it. When a cryptographically relevant quantum computer exists, they decrypt the archive. Diplomatic cables, health records, source code, legal discovery, and financial archives all have shelf lives measured in decades.&lt;/p&gt;

&lt;p&gt;Mosca's Theorem puts it simply: if the time your data must stay secret, plus the time your migration takes, exceeds the time until a cryptographically relevant quantum computer arrives, you are already late.&lt;/p&gt;

&lt;h3&gt;
  
  
  So When Is Q-Day?
&lt;/h3&gt;

&lt;p&gt;Nobody knows, and anyone selling you a date is selling something.&lt;/p&gt;

&lt;p&gt;The most respected longitudinal source is the Global Risk Institute Quantum Threat Timeline, run by Michele Mosca and Marco Piani. Their 2025 edition surveyed 26 experts and put the averaged probability of a cryptographically relevant quantum computer within ten years at 28 to 49 percent, the highest estimate in the report's seven-year history. Most agency planning centers on the 2030 to 2035 window. Some vendor roadmaps are far more aggressive.&lt;/p&gt;

&lt;p&gt;What moved those numbers is not qubit counts. It is resource estimates falling. Estimates for breaking RSA-2048 dropped from roughly 20 million physical qubits in 2019 to under one million in 2025 analyses. The bar is being lowered from the software side while hardware climbs from below.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Deadlines That Already Exist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;NIST IR 8547 (draft transition framework): RSA-2048 and ECC-256 deprecated by 2030, disallowed after 2035.&lt;/li&gt;
&lt;li&gt;NSA CNSA 2.0: quantum-resistant algorithms required for National Security Systems on a timeline anchored to 2030 to 2035.&lt;/li&gt;
&lt;li&gt;AWS: ML-KEM support live in KMS, ACM, and Secrets Manager, with legacy Kyber support removed across endpoints in 2026.&lt;/li&gt;
&lt;li&gt;Cloudflare: post-quantum authentication to origins by mid-2026, visitor-facing by mid-2027.&lt;/li&gt;
&lt;li&gt;Google: 2029 target. Microsoft: full transition by 2033, per The Quantum Insider's roadmap comparison.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  One More Thing Worth Knowing
&lt;/h3&gt;

&lt;p&gt;In July 2026, NIST noted that Anthropic had used an AI model to discover a vulnerability in HAWK, a lattice-based signature scheme that was under consideration for standardization. The HAWK team withdrew it. The finalized standards, ML-KEM and ML-DSA, rest on different foundations and were unaffected.&lt;/p&gt;

&lt;p&gt;I love this story because it captures the whole moment. AI is now fast enough to break cryptography candidates before they ship. That is terrifying and reassuring at the same time, and both feelings are correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passwords Are Still the Front Door
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable truth: while everyone debates quantum timelines, the overwhelming majority of real intrusions still start with a credential.&lt;/p&gt;

&lt;p&gt;Credential phishing is 94 to 96 percent of malicious payloads. Infostealer logs feed criminal marketplaces daily. Password reuse turns one forgotten forum breach into access to your email, and your email is the master key to everything else.&lt;/p&gt;

&lt;p&gt;So what should a password manager actually do in 2026? My criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero knowledge by architecture, not by policy. The vendor should not be able to see your data even if compelled.&lt;/li&gt;
&lt;li&gt;Modern key derivation. Argon2id with a serious memory cost, not fast hashing that GPUs chew through.&lt;/li&gt;
&lt;li&gt;Offline breach checking. Checking your passwords should not require sending anything anywhere.&lt;/li&gt;
&lt;li&gt;Hardware key support. FIDO2 and WebAuthn, ideally bound into key derivation, not bolted on as a login gate.&lt;/li&gt;
&lt;li&gt;Post-quantum key wrapping. Mostly relevant for exports and long-lived wrapped keys, but relevant.&lt;/li&gt;
&lt;li&gt;Open source with reproducible builds. If you cannot read it, you are trusting marketing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most mainstream managers hit two or three of these. Which is why a newer project caught my attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Close Look at PWDnow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://pwdnow.com/" rel="noopener noreferrer"&gt;PWDnow&lt;/a&gt; is an open-source, local-first password manager built by a small team in Montreal. The source lives at &lt;a href="https://github.com/PWDnow/PWDnow-password-manager" rel="noopener noreferrer"&gt;github.com/PWDnow/PWDnow-password-manager&lt;/a&gt; under an MIT license. I want to explain how it works, because the design is genuinely different from what most people are used to, and because understanding it teaches you what zero knowledge is supposed to mean.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Core Idea: A Hard Process Boundary
&lt;/h3&gt;

&lt;p&gt;Most password managers run their cryptography inside the same application that renders the interface. PWDnow splits them.&lt;/p&gt;

&lt;p&gt;A Rust daemon does every cryptographic operation: key derivation, encryption, decryption, storage, memory locking, wiping.&lt;/p&gt;

&lt;p&gt;A React and Express web layer renders the interface and never receives a key of any kind.&lt;/p&gt;

&lt;p&gt;They talk over a Unix domain socket using MessagePack frames. Every request carries a session token, and the daemon verifies the identity of the connecting process at the OS level using SO_PEERCRED, so only the trusted proxy running as the correct system user can reach it.&lt;/p&gt;

&lt;p&gt;The practical consequence: a cross-site scripting bug in the interface cannot leak your vault, because the interface never had it. It only ever held opaque ciphertext.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Key Hierarchy, Step by Step
&lt;/h3&gt;

&lt;p&gt;This is the part worth understanding, because it is how modern vaults should be built.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You type your master password. It goes to the daemon and nowhere else.&lt;/li&gt;
&lt;li&gt;Argon2id derives a Key Encryption Key. The memory cost is set to 1 GiB. That number is deliberate. GPU brute-force farms scale on parallelism, and parallelism gets expensive fast when every guess needs a gigabyte of RAM.&lt;/li&gt;
&lt;li&gt;The Key Encryption Key unwraps the Vault Master Key. The Vault Master Key lives in memory locked with mlock so it cannot be swapped to disk, sealed with mprotect(PROT_NONE) while idle so other processes cannot read the page, and zeroized the moment it is no longer needed.&lt;/li&gt;
&lt;li&gt;The Vault Master Key unwraps per-credential Data Encryption Keys. Every credential gets its own key. Compromising one does not compromise the vault.&lt;/li&gt;
&lt;li&gt;The whole database sits inside a SQLCipher envelope. That is three independent layers before anything touches disk.&lt;/li&gt;
&lt;li&gt;If a hardware security key is registered, the token's HMAC-SHA256 response is folded directly into the derivation, which cryptographically ties the vault to physical possession of the key rather than merely gating the login screen.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Post-Quantum Part
&lt;/h3&gt;

&lt;p&gt;PWDnow runs hybrid key encapsulation by default, not as an opt-in flag. The repository documents hybrid X25519 with ML-KEM-768/1024, and the project site describes an X448 with ML-KEM-1024 configuration targeting NIST Security Level 5 and CNSA 2.0 readiness. Signatures use ML-DSA-87, including nightly root-signing of the audit log chain, where each event is linked by BLAKE3 or SHA-384 hashes.&lt;/p&gt;

&lt;p&gt;There is also a CNSA 2.0 strict mode that restricts the daemon to the NSA's approved suite, removing BLAKE3, SHA3, XChaCha20, Ed25519, and X25519 from active code paths. That is an unusual feature for a consumer-facing tool and a genuinely useful one for anyone with federal or defense obligations.&lt;/p&gt;

&lt;p&gt;Let me be straight with you about what this buys you. Your stored passwords are protected by AES-256-GCM, which is already quantum-resistant enough. The post-quantum layer matters for wrapped keys, exports, and any material that moves between machines or gets backed up somewhere an adversary might be quietly archiving. That is real, but it is not the reason to switch on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Offline Breach Detection
&lt;/h3&gt;

&lt;p&gt;This is my favorite feature, and it solves a problem most people do not know they have.&lt;/p&gt;

&lt;p&gt;When a typical manager checks whether your password appeared in a breach, it sends a partial hash to an online service. That is designed carefully (k-anonymity), but it is still a network call tied to your vault activity.&lt;/p&gt;

&lt;p&gt;PWDnow builds a Cuckoo filter from the Have I Been Pwned password corpus, over 900 million hashes, compressed into a structure small enough to sit on your disk. A Cuckoo filter answers "is this item probably in the set" in constant time with a small, tunable false-positive rate and no false negatives. Lookups happen entirely locally. Zero network calls per password.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duress Mode and Travel Mode
&lt;/h3&gt;

&lt;p&gt;Two features aimed at situations most software pretends do not exist.&lt;/p&gt;

&lt;p&gt;Travel mode hides a chosen subset of credentials behind a separate password. You hand over a device at a border, and the vault that opens is real, complete, and boring.&lt;/p&gt;

&lt;p&gt;Duress mode arms an alternate password that, when entered, triggers a forensic wipe instead of granting access. The project describes this as a NIST SP 800-88 Rev. 2 compliant cryptographic erase using a 32-byte wipe ticket, destroying the SQLCipher key material so the database becomes unrecoverable.&lt;/p&gt;

&lt;p&gt;Handle duress mode with respect. It does exactly what it says.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exports, Imports, and Moving Between Machines
&lt;/h3&gt;

&lt;p&gt;The native format is .p2w: a double-AEAD encrypted file with an inner AES-256-GCM layer and an outer XChaCha20-Poly1305 layer, padded and metadata-obfuscated to resist traffic analysis, with the header bound into both authentication tags. It also imports from Bitwarden, 1Password (CSV and 1PUX), NordPass, and KeePass-compatible XML.&lt;/p&gt;

&lt;p&gt;There is no automatic cloud sync. Moving a vault between devices means exporting the encrypted file and importing it yourself. That is a deliberate trade, and you should decide whether you want it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing It
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/PWDnow/PWDnow-password-manager.git
&lt;span class="nb"&gt;cd &lt;/span&gt;PWDnow
./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer detects your distribution, checks dependencies, audits your SSH configuration, builds daemon and frontend from source, and installs both as systemd services under dedicated unprivileged users. The deployment hardening is thorough: MemorySwapMax=0, NoNewPrivileges, PrivateTmp, only the CAP_IPC_LOCK capability the daemon actually needs, plus an AppArmor profile confining filesystem and capability access at the kernel level.&lt;/p&gt;

&lt;p&gt;Building from source needs Node.js 24+, Rust 1.94+, protoc, and development headers for libsodium, sqlcipher, and libfido2.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Honest Limitations
&lt;/h3&gt;

&lt;p&gt;I am not going to sell you a fantasy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux only.&lt;/strong&gt; Primary testing is on Ubuntu 26.04 LTS, with Debian and RHEL family support in the installer. There is no macOS or Windows build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cloud sync and no mobile app.&lt;/strong&gt; If you live across five devices including a phone, this is not a drop-in replacement for a commercial manager.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is young.&lt;/strong&gt; The repository is early, the star count is low, and CI includes mutation and chaos testing but the project has not published a third-party audit. Open source means you can verify it. It does not mean someone else already did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duress mode is irreversible.&lt;/strong&gt; That is the point, and it is also a foot gun. Keep an encrypted backup somewhere safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Who should use it: Linux users, developers, journalists, researchers, and anyone whose threat model includes device seizure or who simply does not want a vendor holding their vault at all. Who should not: someone who needs seamless phone sync today.&lt;/p&gt;

&lt;p&gt;What I like most is that it is a working reference implementation of how a vault should be built. Even if you never install it, read the architecture doc. It will change what you expect from whatever you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Attack Shapes Nobody Has a Playbook For
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Indirect Prompt Injection
&lt;/h3&gt;

&lt;p&gt;This is the big one, and most teams still underestimate it.&lt;/p&gt;

&lt;p&gt;Direct prompt injection is a user typing malicious instructions into a model. Annoying, mostly contained. Indirect prompt injection is malicious text planted in something an AI agent later reads on its own: a web page, a calendar invite, a support ticket, a PDF, a log line, an issue comment.&lt;/p&gt;

&lt;p&gt;The root cause is architectural. As Sysdig puts it, models cannot reliably distinguish instructions from data, because both arrive as natural language text. OWASP ranked it LLM01, the top risk for LLM applications, and it remains the highest-ranked risk in the 2026 update.&lt;/p&gt;

&lt;p&gt;The pattern behind nearly every real finding: an agent with access to private data, exposure to untrusted content, and the ability to communicate externally. Any two of those is manageable. All three is exploitable.&lt;/p&gt;

&lt;p&gt;It is no longer theoretical. Unit 42 documented the first real-world malicious indirect prompt injection in December 2025. Critical CVEs have landed against Microsoft Copilot (CVSS 9.3), GitHub Copilot (9.6), and Cursor IDE (9.8). Brave's security team demonstrated injections against an AI browser using instructions hidden in white-on-white text and HTML comments, causing it to fetch one-time passcodes from email when the user simply asked it to summarize a page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent Hijack Chains
&lt;/h3&gt;

&lt;p&gt;The useful way to think about this is as a sequence, not a category. Influence the agent, let the agent's existing credentials authorize the action, let the agent's tool access execute it. Old techniques, new privilege boundary.&lt;/p&gt;

&lt;p&gt;The uncomfortable part: you deployed an agent to improve productivity, and it quietly became a privileged execution surface with no audit trail that captures what it did, why, and with what authority.&lt;/p&gt;

&lt;h3&gt;
  
  
  Token and Connector Abuse
&lt;/h3&gt;

&lt;p&gt;Salesloft Drift was the template. Long-lived OAuth tokens bypass MFA entirely, because MFA already happened when the token was issued. Attackers stayed inside API rate limits and deleted bulk query jobs after execution to avoid detection. Now extend that to MCP connectors wiring AI agents into your email, CRM, and code hosts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Propagating Supply Chain Worms
&lt;/h3&gt;

&lt;p&gt;Shai-Hulud infected a package, harvested the maintainer's credentials, found their other packages, injected itself, and published. No human in the loop. Moving execution to pre-install widened the blast radius across developer machines and CI/CD pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI-Assisted Social Engineering
&lt;/h3&gt;

&lt;p&gt;The Microsoft Digital Defense Report 2025 found AI-crafted phishing achieving roughly three times the click-through rate of traditionally written lures. Add voice cloning, and "verify by calling them back" stops being reliable advice unless the callback number came from somewhere you trust.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ransomware That Does Not Want Your Money
&lt;/h3&gt;

&lt;p&gt;One shift I did not expect: some campaigns now aim at destruction rather than extortion. As PKWARE noted, fewer victims are paying because recovery has improved, so some operators pivoted to wiping devices outright. Your backup strategy is now your primary defense, not your fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI in Security: Who Is Actually Winning?
&lt;/h2&gt;

&lt;p&gt;Everyone asks whether AI helps attackers or defenders more. The honest answer is that it helps whoever iterates faster, and right now that advantage is unevenly distributed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Offensive Column
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GTG-1002 ran 80 to 90 percent of an espionage campaign autonomously against roughly 30 targets.&lt;/li&gt;
&lt;li&gt;XBOW, an autonomous offensive security company, reached number one on HackerOne's global leaderboard with over 1,060 validated submissions.&lt;/li&gt;
&lt;li&gt;Attack timelines compressed brutally. Some groups now move from initial access to lateral movement in well under a minute.&lt;/li&gt;
&lt;li&gt;Open-source autonomous pentesting tooling now spans 39 or more active projects, most less than 18 months old. The capability is not gated behind nation-state budgets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Defensive Column
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Google's Big Sleep found the first AI-discovered zero-day in production software, a SQLite buffer underflow that fuzzing had missed for years.&lt;/li&gt;
&lt;li&gt;DARPA's AIxCC finals had systems analyze 54 million lines of code, identify 86 percent of synthetic vulnerabilities, and patch 68 percent. The best-performing team patched 43 of the 54 vulnerabilities their system found. Several finalist systems were open-sourced.&lt;/li&gt;
&lt;li&gt;In February 2026, Anthropic reported more than 500 high-severity vulnerabilities in open source software found with Claude Opus 4.6. AISLE reported 12 OpenSSL zero-days, including a CVSS 9.8 issue dating back to 1998.&lt;/li&gt;
&lt;li&gt;Agentic SOC models are moving from pitch decks to production, with AI handling triage, enrichment, detection rule generation, and first-line investigation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My Read
&lt;/h3&gt;

&lt;p&gt;Three observations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One:&lt;/strong&gt; AI is strongest at exactly the tasks that used to slow attackers down. Reconnaissance, reading unfamiliar code, writing exploit variants, and sorting through exfiltrated data are all expensive human work. They are now cheap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two:&lt;/strong&gt; Defense has a structural advantage nobody talks about. Defenders have source code, build systems, and telemetry. Attackers usually have a binary and a login page. Big Sleep works because it reads the source. If you run the agents on your own code before shipping, you get to find bugs first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three:&lt;/strong&gt; The bottleneck is organizational, not technical. A 43-day median remediation time and 26 percent completion on known-exploited vulnerabilities is not a model capability problem. AI that finds 500 bugs is a burden if your process can only fix five per sprint.&lt;/p&gt;

&lt;p&gt;Whoever fixes the throughput problem wins the next two years.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Expect in 2027
&lt;/h2&gt;

&lt;p&gt;Predictions with my confidence level attached, because unqualified predictions are just marketing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Post-quantum goes visitor-facing. (High confidence)&lt;/strong&gt; Hybrid key exchange is already near half of measured domains. Cloudflare's roadmap targets post-quantum authentication for visitor connections by mid-2027. Expect PQ certificates and signature migration, which is harder than key exchange, to become the active problem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Crypto-agility becomes a procurement requirement. (High confidence)&lt;/strong&gt; The lesson of HAWK's withdrawal is that algorithms can fail on short notice. Expect contracts to start asking how fast you can rotate an algorithm, not just which one you use. Hard-coded crypto in IoT, SCADA, and firmware becomes the visible laggard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Agent identity becomes a real product category. (High confidence)&lt;/strong&gt; Every agent needs an identity, scoped permissions, an audit trail, and a revocation path. Most organizations currently give agents a human's credentials and hope. That will not survive the first large agent-driven breach with a regulator attached.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The first major breach attributed primarily to prompt injection. (Medium-high confidence)&lt;/strong&gt; The ingredients are all present. The only reason it has not been a headline incident yet is disclosure lag and attribution difficulty.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Regulation catches up unevenly. (Medium confidence)&lt;/strong&gt; The EU AI Act's August 2026 obligations start biting, and prompt injection now maps to OWASP, MITRE ATLAS, NIST AI RMF, ISO 42001, GDPR, and NIS2. Expect compliance mapping to become an actual job.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Password managers ship post-quantum wrapping as table stakes. (Medium confidence)&lt;/strong&gt; What PWDnow does today, mainstream managers advertise by late 2027. The bar moves.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No cryptographically relevant quantum computer in 2027. (High confidence, with a caveat)&lt;/strong&gt; The expert consensus centers on the 2030s. But harvest-now-decrypt-later means the deadline that matters to you is set by your data's shelf life plus your migration time, not by Q-Day. If you have data that must stay secret past 2035, your deadline was last year.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI-versus-AI operations become normal. (Medium-high confidence)&lt;/strong&gt; Autonomous attack agents meeting autonomous defense agents, with humans setting policy and reviewing outcomes. The interesting security questions become about oversight, not detection.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Your Action Checklist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  If You Are an Individual
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Get a real password manager and stop reusing passwords. Whether that is PWDnow, Bitwarden, or something else matters far less than actually doing it.&lt;/li&gt;
&lt;li&gt;Turn on phishing-resistant MFA. Hardware keys or passkeys beat SMS by a wide margin.&lt;/li&gt;
&lt;li&gt;Check your passwords against breach corpora. Locally if you can.&lt;/li&gt;
&lt;li&gt;Be suspicious of QR codes. 18.7 million attacks in a single month is not a rounding error.&lt;/li&gt;
&lt;li&gt;Assume voice can be faked. Agree on a verification method with family and finance contacts that does not depend on recognizing a voice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  If You Run a Small Team
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Inventory every OAuth token and third-party integration. Revoke what you do not use. Shorten what you keep.&lt;/li&gt;
&lt;li&gt;Pin dependencies and require review for version bumps. Shai-Hulud spread through automatic updates.&lt;/li&gt;
&lt;li&gt;Give AI agents their own scoped identities. Never a human's credentials.&lt;/li&gt;
&lt;li&gt;Test your restores. Wiper-style ransomware makes backups your primary control.&lt;/li&gt;
&lt;li&gt;Ask vendors one question in writing: what is your post-quantum migration timeline?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  If You Own Enterprise Security
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Build a cryptographic bill of materials. You cannot migrate what you cannot find. Include vendors, embedded systems, and anything where crypto is hidden from you.&lt;/li&gt;
&lt;li&gt;Classify data by required secrecy lifetime. Anything past 2035 goes first.&lt;/li&gt;
&lt;li&gt;Enable hybrid post-quantum TLS where your stack supports it. The overhead is minimal.&lt;/li&gt;
&lt;li&gt;Fix the remediation throughput problem. Finding bugs faster than you fix them makes the queue worse, not the risk lower.&lt;/li&gt;
&lt;li&gt;Treat agent frameworks and MCP connectors as privileged automation platforms, with the logging and approval gates that implies.&lt;/li&gt;
&lt;li&gt;Run offensive AI tooling against yourself. Your adversaries already are, and several of the best systems are open source.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is ML-KEM in simple terms?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ML-KEM is the NIST-standardized way for two parties to agree on a shared secret key that a quantum computer cannot break. It was published as FIPS 203 in August 2024 and comes in three sizes: 512, 768, and 1024. ML-KEM-768 is the common internet default and ML-KEM-1024 is used for the highest security level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is my data at risk from quantum computers today?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not today, but recorded data is. No cryptographically relevant quantum computer exists in 2026. The risk is harvest now, decrypt later: adversaries store encrypted traffic now and decrypt it once the hardware exists. If your data must stay secret into the 2030s, act now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to replace AES-256?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Symmetric encryption like AES-256 and hashing like SHA-2 remain strong against quantum attacks at appropriate key sizes. The migration targets public-key key exchange and digital signatures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between direct and indirect prompt injection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Direct injection is a user typing malicious instructions into an AI system. Indirect injection is malicious instructions hidden in content the AI reads on its own, such as a web page, email, or document. Indirect injection is the bigger enterprise risk because it arrives through normal business data flows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is PWDnow safe to use?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PWDnow is open source under MIT, uses Argon2id key derivation with a 1 GiB memory cost, AES-256-GCM encryption, hybrid X25519/X448 with ML-KEM post-quantum key encapsulation, and isolates all cryptography inside a Rust daemon separate from the interface. It is a young project without a published third-party audit, and it runs on Linux only with no cloud sync. Strong design, real trade-offs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does offline breach detection work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of querying an online breach service, the manager stores a Cuckoo filter built from over 900 million known breached password hashes locally. Lookups happen entirely on your device with no network call per password, so your vault activity never leaves the machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which is worse in 2026, ransomware or credential theft?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Credential theft causes more incidents. Credential phishing accounted for 94 to 96 percent of malicious payload attacks in the second quarter of 2026, while traditional malware made up 4 to 6 percent. Ransomware causes higher per-incident cost, but stolen credentials are how attackers get in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is AI helping attackers more than defenders?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Right now attackers convert AI capability into results faster because their workflow is simpler. Defenders have a structural advantage, including source code access and telemetry, but are limited by remediation throughput. The 2026 Verizon DBIR reported a 43-day median to fully resolve known vulnerabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should my organization start post-quantum migration?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now. NIST's draft transition framework deprecates RSA-2048 and ECC-256 by 2030 and disallows them after 2035, and large enterprise crypto migrations historically take five to fifteen years. Start with a cryptographic inventory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should I do first if I only have one hour?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Change reused passwords on your email, banking, and primary cloud accounts, and turn on hardware-key or passkey MFA for those three. Your email account is the recovery path for everything else, so it deserves your strongest protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;The pattern connecting all of it is trust boundaries collapsing.&lt;/p&gt;

&lt;p&gt;A chatbot integration held a token, so it inherited access to hundreds of Salesforce instances. An AI agent held credentials, so instructions hidden in a webpage inherited them too. A maintainer's npm account held publish rights, so a worm inherited an ecosystem. Public-key cryptography holds the trust of the entire internet, and a future machine may inherit that.&lt;/p&gt;

&lt;p&gt;Every one of these is the same failure: something trusted, delegating without a boundary.&lt;/p&gt;

&lt;p&gt;So the defensive posture that works in 2027 is not a new product. It is a habit. Assume every component will be compromised, and make sure that when it is, what it can reach is small, what it holds is short-lived, and what it did is logged.&lt;/p&gt;

&lt;p&gt;Start with your passwords, because that is still the front door and it takes an hour. Then inventory your cryptography, because that takes a year and the clock already started.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>security</category>
    </item>
    <item>
      <title>DKIM for Developers: A No-Fluff Guide to Email Authentication</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/rabbitholeinphp/dkim-for-developers-a-no-fluff-guide-to-email-authentication-3ac7</link>
      <guid>https://dev.to/rabbitholeinphp/dkim-for-developers-a-no-fluff-guide-to-email-authentication-3ac7</guid>
      <description>&lt;p&gt;No cryptography background needed. Here's what DKIM is, why the selector trips everyone up, the exact records to publish, and a free tool that grades your keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; DKIM (DomainKeys Identified Mail) is a system where your mail server adds a hidden cryptographic signature to every email you send. You publish the matching public key in DNS. The receiving server checks the signature against that key and confirms two things: the message really came from your domain, and nobody altered it along the way. The key lives at an address like &lt;code&gt;selector1._domainkey.yourcompany.com&lt;/code&gt;. And unlike SPF, the signature survives forwarding.&lt;/p&gt;

&lt;p&gt;That's the concept. Now let me show you the part every guide gets wrong: you can't even find your own DKIM record without knowing a secret label called the selector.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem SPF Can't Solve
&lt;/h2&gt;

&lt;p&gt;Last week I walked through SPF. It's the guest list that tells mail servers which machines can send as you. It works. It's essential. And it has one hole you can't patch.&lt;/p&gt;

&lt;p&gt;Somebody forwards your email.&lt;/p&gt;

&lt;p&gt;The message keeps your name in the From field. But now it arrives from their server, which is obviously not on your guest list. SPF fails. Your perfectly legitimate email, forwarded by a happy customer to a colleague, gets flagged as suspicious.&lt;/p&gt;

&lt;p&gt;Now scale that up. Mailing lists forward. Alumni addresses forward. That info@ alias that redirects to three people forwards. Every one of those breaks SPF.&lt;/p&gt;

&lt;p&gt;DKIM is what fixes it. Instead of asking "did this come from an approved server," DKIM asks a completely different question: "was this message signed by the domain owner, and has it been tampered with since?"&lt;/p&gt;

&lt;p&gt;Forward it a hundred times. The signature travels with the message. It still checks out.&lt;/p&gt;

&lt;p&gt;That's why you need both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is DKIM, in Plain English
&lt;/h2&gt;

&lt;p&gt;Forget the acronym. Think about a wax seal on a letter.&lt;/p&gt;

&lt;p&gt;In the old days, you'd fold a letter, drip wax on the flap, and press your family signet ring into it. Two things happened at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Anyone could see the seal was yours because only you had the ring.&lt;/li&gt;
&lt;li&gt;If the seal was broken or smudged, the letter had been opened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;DKIM is that, done with math instead of wax.&lt;/p&gt;

&lt;p&gt;Your mail server has a private key. It's a long secret string that never leaves the server. Every time you send a message, the server runs the message through a calculation using that private key and produces a signature. The signature gets attached to the email in a hidden header nobody sees.&lt;/p&gt;

&lt;p&gt;Your domain publishes the matching public key in DNS, out in the open for anyone to read. Public keys are useless for forging signatures. They can only verify them. That asymmetry is the entire trick.&lt;/p&gt;

&lt;p&gt;The receiving server grabs your public key, runs the check, and gets a clean yes or no.&lt;/p&gt;

&lt;p&gt;DKIM is defined in RFC 6376. The cryptography rules were updated in RFC 8301.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Selector: The Part That Confuses Everyone
&lt;/h2&gt;

&lt;p&gt;Here's the thing that makes DKIM different from every other DNS record you've dealt with.&lt;/p&gt;

&lt;p&gt;With SPF, you look up yourcompany.com and you find the record. Done.&lt;/p&gt;

&lt;p&gt;With DKIM, there's no fixed address. The public key sits at a location that includes an arbitrary label the sender chose, called a selector. The full address looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selector1._domainkey.yourcompany.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;_domainkey&lt;/code&gt; part is fixed. Everyone uses it. But &lt;code&gt;selector1&lt;/code&gt; is just a name somebody picked. It could be &lt;code&gt;google&lt;/code&gt;. It could be &lt;code&gt;pm&lt;/code&gt;. It could be &lt;code&gt;k1&lt;/code&gt;, or &lt;code&gt;fm2&lt;/code&gt;, or &lt;code&gt;s2026a&lt;/code&gt;, or &lt;code&gt;bananas&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why does this design exist? Because one domain usually sends mail through several different services, and each one needs its own key. Your main email platform signs with one selector. Your marketing tool signs with another. Your invoicing system with a third. Separate selectors mean you can rotate or shut off one platform without touching any of the others.&lt;/p&gt;

&lt;p&gt;Why is this annoying? Because DNS provides no way to list every name under a branch. You can't ask "show me all the DKIM keys for this domain." There is no such query. If you don't know the selector, you can't look it up, full stop.&lt;/p&gt;

&lt;p&gt;This one design choice is responsible for most of the confusion around DKIM. Once you understand it, the rest is easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters Right Now
&lt;/h2&gt;

&lt;p&gt;DKIM is the least deployed of the three protocols. A February 2026 scan of 5,499,028 domains from the Tranco top sites list found DKIM adoption at just 22.7%, compared with 56.0% for SPF and 30.4% for DMARC, according to DMARCguard's State of Email Authentication research.&lt;/p&gt;

&lt;p&gt;Read that again. More than three quarters of the internet's most visited domains publish no verifiable signing key at all.&lt;/p&gt;

&lt;p&gt;Email fraud is not slowing down. The FBI's Internet Crime Complaint Center recorded over one million complaints and $20.88 billion in losses in 2025. Business email compromise accounted for $3,046,598,558 from 24,768 complaints, second only to investment fraud. The full data is in the 2025 IC3 Annual Report.&lt;/p&gt;

&lt;p&gt;AI joined the party officially. That same report added an AI-related crime category for the first time in 25 years: 22,364 complaints and $893,346,472 in losses. Writing a convincing impersonation of your CFO no longer requires talent.&lt;/p&gt;

&lt;p&gt;Email is still the front door. Verizon's 2026 Data Breach Investigations Report analyzed more than 22,000 confirmed breaches and found the human element in 62% of them, with phishing behind 16% of initial access. Among AI-assisted intrusions, phishing was the largest single category at 44%.&lt;/p&gt;

&lt;p&gt;And the mailbox providers now require it. Google and Yahoo started enforcing authentication for senders above 5,000 messages a day in February 2024. Microsoft followed on May 5, 2025, rejecting non-compliant mail to Outlook.com, Hotmail.com and Live.com outright with the error &lt;code&gt;550 5.7.515 Access denied&lt;/code&gt;, as documented in Microsoft's announcement. Google's rules live in the Gmail sender guidelines.&lt;/p&gt;

&lt;p&gt;Both explicitly require DKIM. Not SPF or DKIM. DKIM.&lt;/p&gt;

&lt;h2&gt;
  
  
  How DKIM Works, Step by Step
&lt;/h2&gt;

&lt;p&gt;Here's what happens between you hitting send and the message landing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1. Your server signs the message.&lt;/strong&gt; Before it goes out, your mail platform selects a set of headers (usually From, Subject, Date, To and a few others) plus the body, runs them through a hashing calculation, and encrypts the result with its private key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2. It attaches the signature.&lt;/strong&gt; A hidden header called &lt;code&gt;DKIM-Signature&lt;/code&gt; is added to the message. Inside it are the signing domain (&lt;code&gt;d=&lt;/code&gt;), the selector (&lt;code&gt;s=&lt;/code&gt;), the list of headers that were signed (&lt;code&gt;h=&lt;/code&gt;), a hash of the body (&lt;code&gt;bh=&lt;/code&gt;), and the signature itself (&lt;code&gt;b=&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3. The message travels.&lt;/strong&gt; Through your outbound server, through relays, possibly through a forwarder or two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4. The receiving server reads the header.&lt;/strong&gt; It pulls out &lt;code&gt;d=&lt;/code&gt; and &lt;code&gt;s=&lt;/code&gt; and builds the DNS address: &lt;code&gt;&amp;lt;selector&amp;gt;._domainkey.&amp;lt;domain&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5. It fetches your public key.&lt;/strong&gt; One DNS lookup. Milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6. It recalculates and compares.&lt;/strong&gt; It hashes the same headers and body the sender did, decrypts your signature with the public key, and checks whether the two match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7. Result.&lt;/strong&gt; Match means the message is authentic and unmodified. No match means something changed in transit, or the signature was never valid to begin with.&lt;/p&gt;

&lt;p&gt;The whole thing is invisible to the person reading the email. It just quietly decides whether your message is trusted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DKIM Does Not Do
&lt;/h2&gt;

&lt;p&gt;This section matters. Skip it and you'll overestimate what you just installed.&lt;/p&gt;

&lt;p&gt;DKIM does not prove who sent the message from where. It proves the signature is valid. That's a subtly different thing, and attackers have learned to exploit the gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DKIM signatures can be replayed.&lt;/strong&gt; This is the big one. A DKIM signature has no timestamp and no recipient binding. If an attacker gets hold of a legitimately signed message, they can resend that exact message to thousands of new victims and the signature still verifies.&lt;/p&gt;

&lt;p&gt;This is not theoretical. In April 2025, attackers pulled off a live DKIM replay attack against Google itself. They registered a domain, created an OAuth app whose name contained an entire phishing message, and triggered Google's automated security alert. Google generated that alert and signed it with a valid DKIM signature. The attackers then relayed the untouched message onward, and it landed in Gmail inboxes showing as signed by Google, complete with a fake subpoena notice. Abnormal AI published a full technical breakdown.&lt;/p&gt;

&lt;p&gt;Authentication passed. The email was still a phish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DKIM breaks when the message body is modified.&lt;/strong&gt; Mailing lists that append a footer, or gateways that rewrite links, change the body and invalidate the body hash. Signature fails, and nothing was malicious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DKIM does not stop spoofing on its own.&lt;/strong&gt; A signature can verify perfectly while the visible From address belongs to someone else entirely. Only DMARC forces the signing domain to align with what the recipient actually sees.&lt;/p&gt;

&lt;p&gt;So DKIM is not a magic wand. It's the durable half of a three part system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Should Set Up DKIM: The Real Benefits
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your email survives forwarding.&lt;/strong&gt; The single biggest practical win. SPF cannot do this. DKIM can.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tamper evidence.&lt;/strong&gt; If anything modifies your message in transit, the signature breaks and the recipient's server knows.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You meet the bulk sender requirements.&lt;/strong&gt; Google, Yahoo and Microsoft all require it above 5,000 messages a day, and increasingly expect it below that threshold too.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DMARC becomes reliable.&lt;/strong&gt; DMARC passes when either SPF or DKIM aligns. With both published, a forwarded message that fails SPF still passes on DKIM. Your legitimate mail stops getting caught in the crossfire.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better sender reputation.&lt;/strong&gt; Enterprise gateways score signing configuration, not just pass or fail. A properly signed 2048-bit message reads as a maintained, professional operation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You can revoke a compromised sender instantly.&lt;/strong&gt; If a marketing platform gets breached, you retire that one selector and every other sending stream keeps working.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It costs nothing.&lt;/strong&gt; Your mail provider generates the key. You publish one DNS record. No license, no subscription.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compliance.&lt;/strong&gt; PCI DSS 4.0, NIS2 and DORA all treat email authentication as a required control.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Every Tag Inside a DKIM Record, Decoded
&lt;/h2&gt;

&lt;p&gt;Your DKIM DNS record is a list of short tags separated by semicolons. Here they all are.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;What it means in plain English&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version declaration. Says "this is a DKIM record."&lt;/td&gt;
&lt;td&gt;Technically optional, but if present it must come first.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;k=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The type of key. Usually &lt;code&gt;rsa&lt;/code&gt;. Sometimes &lt;code&gt;ed25519&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Ed25519 keys look tiny compared to RSA. Short does not mean weak.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The public key itself. This is the long block of gibberish.&lt;/td&gt;
&lt;td&gt;An empty &lt;code&gt;p=&lt;/code&gt; revokes the key. A truncated key fails silently.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;t=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Testing mode. Tells receivers to ignore failures.&lt;/td&gt;
&lt;td&gt;Belongs in the first hour of setup and nowhere else. Remove it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;h=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which hash algorithms are allowed.&lt;/td&gt;
&lt;td&gt;Never restrict to sha1. It is deprecated.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Service type. Almost always &lt;code&gt;email&lt;/code&gt; or &lt;code&gt;*&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Rarely set. Harmless.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;n=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A free text note for administrators.&lt;/td&gt;
&lt;td&gt;Ignored by every verifier. Purely a comment.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The tags inside the message itself
&lt;/h3&gt;

&lt;p&gt;You'll also encounter tags in the &lt;code&gt;DKIM-Signature&lt;/code&gt; header on an actual email. These are the ones worth recognizing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The signing domain. This is what has to align with your From address for DMARC to pass.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The selector. This is how you find your key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;h=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The list of headers that were signed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bh=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The body hash. If the body changes, this breaks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;b=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The actual signature.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Reading a real record
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=DKIM1; k=rsa; h=sha256; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In English: this is a DKIM record, using an RSA key, with SHA-256 hashing, and here is the public key. Clean and correct.&lt;/p&gt;

&lt;p&gt;Now here's one that's quietly broken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=DKIM1; k=rsa; t=y; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;t=y&lt;/code&gt; means testing mode is still on, so receivers have been told to ignore any failure. You're signing every message and getting zero benefit from it. This is one of the most common findings on a domain that "has DKIM set up."&lt;/p&gt;

&lt;p&gt;And this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=DKIM1; k=rsa; p=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty &lt;code&gt;p=&lt;/code&gt;. The key is revoked. If you're still signing with this selector, every message you send is failing DKIM right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Find Your DKIM Selector
&lt;/h2&gt;

&lt;p&gt;Three reliable methods, in order of certainty.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 1: Read it from a message you sent (definitive)
&lt;/h3&gt;

&lt;p&gt;Send yourself an email. Open it in Gmail, click the three dots, choose Show original. Find the &lt;code&gt;DKIM-Signature&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;s=&lt;/code&gt; tag is your selector. The &lt;code&gt;d=&lt;/code&gt; tag is your signing domain. This is the only method that cannot be wrong, because it tells you what your server is actually doing right now, not what it's supposed to be doing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 2: Check your provider's setup instructions
&lt;/h3&gt;

&lt;p&gt;When your mail platform asked you to add a DNS record, the hostname contained the selector. Look for something ending in &lt;code&gt;._domainkey&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 3: Guess the common ones
&lt;/h3&gt;

&lt;p&gt;Most hosted platforms use predictable selectors. Here are the ones you'll encounter most:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Selector&lt;/th&gt;
&lt;th&gt;Full hostname&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Workspace&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google._domainkey.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft 365&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;selector1&lt;/code&gt;, &lt;code&gt;selector2&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;selector1._domainkey.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zoho Mail&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;zoho&lt;/code&gt;, &lt;code&gt;zmail&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zoho._domainkey.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proton Mail&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;protonmail&lt;/code&gt;, &lt;code&gt;protonmail2&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;protonmail._domainkey.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fastmail&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fm1&lt;/code&gt;, &lt;code&gt;fm2&lt;/code&gt;, &lt;code&gt;fm3&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fm1._domainkey.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SendGrid&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;s1&lt;/code&gt;, &lt;code&gt;s2&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s1._domainkey.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailchimp / Mandrill&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;k1&lt;/code&gt;, &lt;code&gt;k2&lt;/code&gt;, &lt;code&gt;mandrill&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;k1._domainkey.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postmark&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pm&lt;/code&gt;, &lt;code&gt;pic&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pm._domainkey.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SES&lt;/td&gt;
&lt;td&gt;three rotating tokens&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;token&amp;gt;._domainkey.example.com&lt;/code&gt;, published as CNAMEs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted servers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;default&lt;/code&gt;, &lt;code&gt;mail&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default._domainkey.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A useful side note for anyone doing competitive research: a selector name usually identifies the platform behind it. Finding &lt;code&gt;k1._domainkey&lt;/code&gt; on a domain tells you they use Mailchimp. That's a public fact sitting in DNS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Tutorial: Publishing Your DKIM Record
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: List every service that sends email as you
&lt;/h3&gt;

&lt;p&gt;Same exercise as SPF, and just as important. Each one needs its own key and its own selector.&lt;/p&gt;

&lt;p&gt;Your main email platform. Your marketing tool. Your transactional sender. Your CRM. Your helpdesk. Your invoicing software. Your ecommerce order confirmations. Your monitoring alerts. Anything that sends without a signature is mail your recipients cannot verify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Generate the key inside your provider
&lt;/h3&gt;

&lt;p&gt;Do not generate keys by hand unless you run your own mail server. Every hosted platform has a DKIM setup screen that generates the key pair for you, keeps the private half, and shows you exactly what to publish.&lt;/p&gt;

&lt;p&gt;Roughly where to look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Workspace:&lt;/strong&gt; Admin console, Apps, Google Workspace, Gmail, Authenticate email&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft 365:&lt;/strong&gt; Defender portal, Email and collaboration, Policies, Email authentication settings, DKIM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendGrid, Postmark, Mailchimp, Brevo, Klaviyo:&lt;/strong&gt; usually under Sender Authentication or Domain Verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the tool offers you a key length, choose 2048-bit. Always. More on this below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Publish it in DNS
&lt;/h3&gt;

&lt;p&gt;Your provider will hand you either a TXT record or a CNAME record. These are handled differently and it matters.&lt;/p&gt;

&lt;p&gt;If they give you a TXT record, create it like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What to enter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Name / Host&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;selector._domainkey&lt;/code&gt; (for example &lt;code&gt;google._domainkey&lt;/code&gt;). Most panels append your domain automatically.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value / Content&lt;/td&gt;
&lt;td&gt;The full string starting with &lt;code&gt;v=DKIM1;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL&lt;/td&gt;
&lt;td&gt;3600 or Auto&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If they give you a CNAME record, which is how Microsoft, SendGrid, Fastmail and Amazon SES all do it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What to enter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;CNAME&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Name / Host&lt;/td&gt;
&lt;td&gt;&lt;code&gt;selector1._domainkey&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value / Target&lt;/td&gt;
&lt;td&gt;The long hostname your provider gave you&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A CNAME is a pointer. It tells DNS to go look at the provider's own record instead of storing a copy. This is strictly better, because the provider can rotate the underlying key on their schedule without ever touching your DNS.&lt;/p&gt;

&lt;p&gt;Never convert a CNAME into a TXT copy. People do this when their DNS panel is fussy. It works right up until the provider rotates the key, and then your mail fails DKIM with no warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Handle the 255 character problem
&lt;/h3&gt;

&lt;p&gt;This is the trap that catches almost everyone using TXT records.&lt;/p&gt;

&lt;p&gt;A 2048-bit public key is around 392 characters. A single DNS TXT string is limited to 255 characters. So the record has to be split into multiple quoted chunks inside one record, and DNS joins them back together automatically.&lt;/p&gt;

&lt;p&gt;Most modern DNS panels do this for you invisibly. Some do not, and some silently truncate the value instead of warning you.&lt;/p&gt;

&lt;p&gt;The failure mode is nasty: your mail still sends, your logs stay clean, nothing looks wrong on your end, and every receiving server rejects the signature. This is why step 6 exists.&lt;/p&gt;

&lt;p&gt;Do not solve this by dropping to a 1024-bit key. Split the string, or use a CNAME, or move to a DNS host that handles it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Turn on signing and remove testing mode
&lt;/h3&gt;

&lt;p&gt;Publishing the key does nothing by itself. Go back into your provider and switch signing on. In Google Workspace this is a button labeled Start authentication.&lt;/p&gt;

&lt;p&gt;If your record contains &lt;code&gt;t=y&lt;/code&gt;, remove it once you've confirmed signatures verify. Testing mode tells the world to ignore your failures, which cancels out the entire point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Test it, and do not skip this
&lt;/h3&gt;

&lt;p&gt;Verify with the free DKIM Record Checker from &lt;a href="https://www.emailverifyerapi.com/dkim-record-checker" rel="noopener noreferrer"&gt;EmailVerifyerAPI&lt;/a&gt;. It solves the selector problem, which is why it earns a spot here.&lt;/p&gt;

&lt;p&gt;Leave the selector field blank and it probes around thirty selectors used by Google, Microsoft, Zoho, Postmark, Mailchimp, Fastmail, SendGrid and others, then reports what it finds. If you already know your selector, enter it for a definitive answer.&lt;/p&gt;

&lt;p&gt;Here's what you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A score out of 100 with a band from Missing through Weak, Low, Normal, Strong and Excellent, rated on key hygiene rather than mere presence.&lt;/li&gt;
&lt;li&gt;Selector discovery, so you can audit a domain without knowing its setup in advance.&lt;/li&gt;
&lt;li&gt;Key strength assessment, flagging 1024-bit keys that should be rotated.&lt;/li&gt;
&lt;li&gt;Revocation detection, catching the empty &lt;code&gt;p=&lt;/code&gt; tag that means a key is retired but still in use.&lt;/li&gt;
&lt;li&gt;Testing mode detection, catching &lt;code&gt;t=y&lt;/code&gt; left on from setup day.&lt;/li&gt;
&lt;li&gt;DMARC context, because a signature with no DMARC policy behind it has no consequence.&lt;/li&gt;
&lt;li&gt;A numbered findings list, each item explaining what is wrong and the specific change to make.&lt;/li&gt;
&lt;li&gt;No account, no signup, nothing stored on their side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing I respect about it: when you supply no selector and it finds nothing, it reports that as inconclusive rather than scoring you a zero. That's the honest answer, because DNS genuinely cannot enumerate names under a subtree, and a blank result means the guesses missed, not that no key exists. Tools that report "no DKIM found" in that situation are lying to you.&lt;/p&gt;

&lt;p&gt;How to use it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the DKIM record checker.&lt;/li&gt;
&lt;li&gt;Enter your domain. Add your selector if you know it, leave it blank if you don't.&lt;/li&gt;
&lt;li&gt;Read the band first. Anything below Normal needs attention.&lt;/li&gt;
&lt;li&gt;Work down the findings list. Revoked keys and testing mode first, key length second.&lt;/li&gt;
&lt;li&gt;Re-run after each DNS change.&lt;/li&gt;
&lt;li&gt;Then check your own domain against the SPF and DMARC checkers on the same site, because DKIM alone is not a finished job.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 7: Confirm with a real message
&lt;/h3&gt;

&lt;p&gt;Send an email to a Gmail address you control. Show original. Look for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DKIM: 'PASS' with domain yourcompany.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the domain shown next to DKIM is not your domain, you have a signature that verifies but does not align, and DMARC will not pass on it. That usually means your platform is signing with its own domain instead of yours, and you need to finish the branded sending setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes a Perfect, Strong DKIM Setup
&lt;/h2&gt;

&lt;p&gt;Eight characteristics. All of them checkable in a few minutes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Every sending service signs.&lt;/strong&gt; Not just your main mail platform. Your invoices and order confirmations matter more than your newsletter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keys are 2048-bit RSA.&lt;/strong&gt; RFC 8301 updated the cryptographic requirements for DKIM, and 1024-bit has been below NIST's recommended minimum since 2013. A 1024-bit signature still verifies almost everywhere, but enterprise gateways like Proofpoint and Mimecast factor key strength into sender scoring. It's a visible signal that nobody has looked at your setup in a decade.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One selector per service.&lt;/strong&gt; Shared keys mean you cannot revoke one platform without breaking the rest.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Selector names are versioned or dated.&lt;/strong&gt; Something like &lt;code&gt;s2026a&lt;/code&gt; instead of &lt;code&gt;default&lt;/code&gt;. Future you will know instantly which key is current.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No &lt;code&gt;t=y&lt;/code&gt; anywhere.&lt;/strong&gt; Testing mode off.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No stale or revoked keys still in use.&lt;/strong&gt; An empty &lt;code&gt;p=&lt;/code&gt; on a selector you are actively signing with means silent, total DKIM failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The signing domain aligns with your From address.&lt;/strong&gt; Verify signature is not the same as DMARC passes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keys rotate on a schedule.&lt;/strong&gt; Every 6 to 12 months is the widely accepted practice.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to Rotate a Key Without Breaking Anything
&lt;/h2&gt;

&lt;p&gt;Follow the order exactly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Publish the new key at a NEW selector: &lt;code&gt;s2026b._domainkey&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wait for the old TTL to expire&lt;/li&gt;
&lt;li&gt;Switch your platform to sign with the new selector&lt;/li&gt;
&lt;li&gt;Revoke the old key, do NOT delete it: &lt;code&gt;v=DKIM1; k=rsa; p=&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 4 is the one people get wrong. Revoking means publishing the record with an empty &lt;code&gt;p=&lt;/code&gt; value. That's the defined way to retire a key.&lt;/p&gt;

&lt;p&gt;Deleting the record instead leaves any messages still in transit unverifiable rather than definitively unsigned, and it erases the evidence that the selector was ever yours. Keep the revoked record published.&lt;/p&gt;

&lt;h2&gt;
  
  
  On Ed25519 Keys
&lt;/h2&gt;

&lt;p&gt;You may see the option for Ed25519, a newer and much shorter key type. The keys look almost suspiciously small compared to RSA. They are not weak. They are a different kind of mathematics.&lt;/p&gt;

&lt;p&gt;The catch is that support across receivers and sending platforms is still uneven in 2026. If you want to use them, dual sign: publish Ed25519 alongside RSA-2048 so receivers that do not support the newer type fall back gracefully. Do not switch to Ed25519 alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Eight Mistakes I See Most Often
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The key was published but signing was never switched on.&lt;/strong&gt; The record exists, the platform never activated. Extremely common.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;t=y&lt;/code&gt; left on forever.&lt;/strong&gt; You are signing everything and getting nothing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A truncated 2048-bit key.&lt;/strong&gt; The DNS panel silently cut the value at 255 characters. Everything looks fine and nothing verifies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;1024-bit keys nobody has revisited.&lt;/strong&gt; Some hosting panels still generate these by default.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A CNAME flattened into a TXT copy.&lt;/strong&gt; Works until the provider rotates. Then it doesn't.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One selector shared across every platform.&lt;/strong&gt; Cannot revoke anything without breaking everything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Signing with the provider's domain instead of yours.&lt;/strong&gt; Signature passes, alignment fails, DMARC fails.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set up once, never checked again.&lt;/strong&gt; Providers rotate keys. Teams add tools. Nobody is watching.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  SPF, DKIM and DMARC: How the Three Fit Together
&lt;/h2&gt;

&lt;p&gt;One line each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SPF&lt;/strong&gt; answers: is this server allowed to send for this domain? It's the guest list. It breaks on forwarding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DKIM&lt;/strong&gt; answers: was this message signed by the domain owner, and is it unaltered? It's the wax seal. It survives forwarding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DMARC&lt;/strong&gt; answers: what should receivers do when the others fail, and does the authenticated domain match the From address the human sees? It's the policy and reporting layer.&lt;/p&gt;

&lt;p&gt;DMARC passes if either SPF or DKIM passes and aligns with your visible From domain. That's the whole reason to publish both. When one breaks, the other carries the message.&lt;/p&gt;

&lt;p&gt;The gap in the real world is enormous. DMARCguard found only 12.8% of scanned domains actually enforce a policy that stops spoofing. As Valimail notes in its 2026 assessment, strong authentication is defined by alignment and governance, not by the presence of DNS records alone.&lt;/p&gt;

&lt;p&gt;Publishing a key is the easy part. Making it mean something takes DMARC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is a DKIM record in simple terms?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a public key published in your domain's DNS that lets receiving mail servers verify a cryptographic signature your mail server adds to every message. If the signature checks out, the message genuinely came from your domain and was not altered in transit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a DKIM selector?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An arbitrary label chosen by the sender that identifies which key to use, allowing one domain to publish many keys. It appears in the DNS hostname as &lt;code&gt;&amp;lt;selector&amp;gt;._domainkey.&amp;lt;domain&amp;gt;&lt;/code&gt; and in the &lt;code&gt;s=&lt;/code&gt; tag of the message's DKIM-Signature header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I find my DKIM selector?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Send yourself an email and view the raw source. In the DKIM-Signature header, the &lt;code&gt;s=&lt;/code&gt; tag is your selector. Your mail provider's DNS setup instructions also name it, for example &lt;code&gt;google&lt;/code&gt; for Google Workspace or &lt;code&gt;selector1&lt;/code&gt; for Microsoft 365.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why can't a checker find my DKIM record automatically?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because DNS offers no way to list names under a subtree. Selectors are arbitrary strings, so a tool can only guess. A checker that probes common selectors covers most hosted platforms, but a blank result means the guesses missed, not that no key exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should DKIM keys be 1024-bit or 2048-bit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use 2048-bit RSA. 1024-bit keys still verify at most receivers but fall below NIST's minimum and are treated as weak by security-conscious gateways. If your DNS provider rejects the longer value, split the &lt;code&gt;p=&lt;/code&gt; tag into multiple 255-character strings inside one record rather than downgrading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does an empty p= tag mean?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It revokes the key. Publishing &lt;code&gt;p=&lt;/code&gt; with no value is the defined way to retire a selector. Signatures made with that key stop verifying while the record itself stays in place. If you see an empty &lt;code&gt;p=&lt;/code&gt; on a selector you still sign with, your mail is failing DKIM right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is DKIM testing mode (t=y)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It tells receivers the domain is still testing and that failures should not affect handling. Useful during the first hour of setup, harmful afterwards, because it neutralizes the value of the signature entirely. Remove it once you confirm signatures verify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need DKIM if I already have SPF?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. SPF breaks whenever a message is forwarded, because the relaying server is not on your list. A DKIM signature travels with the message and still verifies after forwarding. DMARC passes when either aligns, so publishing both is what keeps legitimate mail delivering in the real world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does DKIM stop email spoofing completely?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. A signature can verify perfectly while the visible From address is forged, and DKIM signatures can be captured and replayed to new recipients because they carry no timestamp or recipient binding. DMARC is what ties the signature to the address the recipient actually sees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a DKIM replay attack?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An attacker takes a legitimately signed message and resends it unmodified to new victims. Because nothing changed, the signature still verifies and the message passes authentication. The April 2025 campaign that spoofed &lt;a href="mailto:no-reply@google.com"&gt;no-reply@google.com&lt;/a&gt; worked exactly this way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often should I rotate DKIM keys?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every 6 to 12 months. Publish the new key at a new selector, wait for propagation, switch signing, then revoke the old key with an empty &lt;code&gt;p=&lt;/code&gt; rather than deleting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is DKIM free?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Your mail provider generates the key pair at no cost, and the public key is a DNS record on a domain you already pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your 30 Minute Action Plan
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run your domain through the free DKIM checker. Leave the selector blank if you don't know it. Two minutes.&lt;/li&gt;
&lt;li&gt;Send yourself an email and read the DKIM-Signature header to confirm your real selector and signing domain. Three minutes.&lt;/li&gt;
&lt;li&gt;List every service that sends email as you. Five minutes.&lt;/li&gt;
&lt;li&gt;Generate a 2048-bit key in each platform that is not yet signing. Ten minutes.&lt;/li&gt;
&lt;li&gt;Publish each record, using CNAME wherever the provider offers it. Five minutes.&lt;/li&gt;
&lt;li&gt;Remove any &lt;code&gt;t=y&lt;/code&gt; you find, and revoke any stale selector properly with an empty &lt;code&gt;p=&lt;/code&gt;. Three minutes.&lt;/li&gt;
&lt;li&gt;Re-run the checker. Confirm Strong or Excellent. Two minutes.&lt;/li&gt;
&lt;li&gt;Set a calendar reminder for six months out, titled "rotate DKIM keys."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then finish the job with DMARC, because a signature nobody acts on is just decoration.&lt;/p&gt;

&lt;p&gt;If you haven't checked your SPF record yet, do that first. DKIM is the durable half of email authentication, but SPF is the foundation, and DMARC is what turns either of them into actual protection.&lt;/p&gt;

</description>
      <category>security</category>
      <category>email</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>SPF Records: Why Your 10 DNS Lookups Break (And The Fix)</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/rabbitholeinphp/spf-records-why-your-10-dns-lookups-break-and-the-fix-3bo1</link>
      <guid>https://dev.to/rabbitholeinphp/spf-records-why-your-10-dns-lookups-break-and-the-fix-3bo1</guid>
      <description>&lt;p&gt;Your SPF record is a lie.&lt;/p&gt;

&lt;p&gt;Not because you wrote it wrong. But because the guest list you published three years ago no longer matches the party you are throwing today.&lt;/p&gt;

&lt;p&gt;Here is the uncomfortable truth: &lt;strong&gt;76% of title tags get rewritten by Google&lt;/strong&gt;, and a similar identity crisis happens with SPF records. You set it and forget it. The infrastructure changes. The record doesn't. And then one Tuesday, your transactional emails start landing in spam.&lt;/p&gt;

&lt;p&gt;This is not an SPF problem. This is a &lt;strong&gt;reality drift&lt;/strong&gt; problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hidden Reason Your SPF Fails (And It Is Not Syntax)
&lt;/h3&gt;

&lt;p&gt;Most developers think SPF is a syntax check. It is not. SPF is an &lt;strong&gt;identity authorization system&lt;/strong&gt;. It asks one question: &lt;em&gt;Is this server allowed to send mail for this domain?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But here is the part the tutorials skip: &lt;strong&gt;SPF does not authenticate the &lt;code&gt;From:&lt;/code&gt; address you see in your inbox.&lt;/strong&gt; It authenticates the &lt;code&gt;MAIL FROM&lt;/code&gt; envelope identity. That gap is where phishing lives and where your legitimate mail gets caught in the crossfire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SPF authorizes the infrastructure. DMARC connects that authorization to the visible sender.&lt;/strong&gt; Without both, you are running a security checkpoint that lets people in but never checks their ID against the name on the list.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 10-Lookup Time Bomb (And Why Your Include Chain Is a House of Cards)
&lt;/h3&gt;

&lt;p&gt;Here is the number that breaks production environments: &lt;strong&gt;10&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SPF evaluation is limited to &lt;strong&gt;10 DNS-query-causing mechanisms&lt;/strong&gt;, including nested &lt;code&gt;include:&lt;/code&gt; records. Exceed it, and you get &lt;code&gt;permerror&lt;/code&gt;. Your mail bounces. No warning. No grace period.&lt;/p&gt;

&lt;p&gt;You start with this, and it feels fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 include:_spf.google.com ~all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you add a CRM. A help desk. A transactional provider. A marketing platform. Everything looks reasonable on the surface.&lt;/p&gt;

&lt;p&gt;But that &lt;code&gt;include:&lt;/code&gt; is not one lookup. It is a tree. The provider's SPF record contains another &lt;code&gt;include:&lt;/code&gt;. That one contains three more. The visible record says three. The evaluation tree says eleven.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A provider changing its internal SPF structure can silently break your domain overnight.&lt;/strong&gt; That is the hidden cost of SPF complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 20-Minute Fix (The "Small But True" Method)
&lt;/h3&gt;

&lt;p&gt;Forget the 5,000-word guides. This is the exact sequence that works in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Kill the dead weight.&lt;/strong&gt;&lt;br&gt;
You have vendors in your SPF that you stopped paying six months ago. Remove them. Every unnecessary &lt;code&gt;include:&lt;/code&gt; is a lookup you cannot afford.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Expand the tree manually.&lt;/strong&gt;&lt;br&gt;
Do not trust the top-level view. Use a flattening tool or a lookup checker to see the &lt;em&gt;actual&lt;/em&gt; evaluation count, not the visible line count. If you are over 10, you are already broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Move from &lt;code&gt;~all&lt;/code&gt; to &lt;code&gt;-all&lt;/code&gt; only when you have earned it.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;~all&lt;/code&gt; is a softfail. It says "probably not authorized." &lt;code&gt;-all&lt;/code&gt; is a hard fail. It says "definitively not authorized." Do not move to &lt;code&gt;-all&lt;/code&gt; until you have inventoried every legitimate sender. If you miss one, you will block your own sales team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Stop using &lt;code&gt;ptr&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
RFC 7208 explicitly discourages it. It is slow, unreliable, and a lookup sink. Use &lt;code&gt;ip4&lt;/code&gt;, &lt;code&gt;ip6&lt;/code&gt;, or provider-specific &lt;code&gt;include:&lt;/code&gt; mechanisms instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Technical Reference Table (For The Skimmers)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;th&gt;Lookup Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delegates to another domain's SPF&lt;/td&gt;
&lt;td&gt;Yes (nested)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ip4:&lt;/code&gt; / &lt;code&gt;ip6:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Authorizes a specific IP or range&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Authorizes the domain's A/AAAA records&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Authorizes the domain's MX records&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ptr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reverse DNS check (avoid)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exists:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DNS query for a specific record&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;-all&lt;/code&gt; / &lt;code&gt;~all&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Catch-all policy&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The 2026 Reality Check: Compliance Is Not Optional
&lt;/h3&gt;

&lt;p&gt;Gmail and Microsoft have made SPF, DKIM, and DMARC table stakes for bulk senders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gmail&lt;/strong&gt; requires SPF, DKIM, and DMARC for anyone sending &lt;strong&gt;5,000+ messages per day&lt;/strong&gt; to Gmail accounts. This has been enforced since February 2024 and tightened through 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microsoft&lt;/strong&gt; began enforcing the same requirement for high-volume senders to Outlook.com, Hotmail, and Live.com in &lt;strong&gt;May 2025&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The consequence is simple: if you are sending at volume and your SPF is broken, you are not just losing deliverability. You are losing access.&lt;/p&gt;

&lt;h3&gt;
  
  
  What AI Search Engines Actually Cite (GEO Matters Now)
&lt;/h3&gt;

&lt;p&gt;Here is the 2026 twist: your article is not just read by humans. It is parsed by ChatGPT, Perplexity, and Google AI Overviews.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generative Engine Optimization (GEO) rewards self-contained, extractable paragraphs.&lt;/strong&gt; AI engines do not rank pages. They extract answers. A clear, factual statement like "SPF evaluation is limited to 10 DNS-query-causing mechanisms" is citable. A rambling 500-word intro is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your SPF record should describe reality.&lt;/strong&gt; That sentence is the most citable takeaway in this entire article. It is a belief statement, not a technical spec. AI engines love belief statements because they are memorable and attributable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The One Rule to Remember
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Do not build the longest SPF record. Build the smallest accurate one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your SPF record is a living document. It should represent the systems that &lt;em&gt;actually&lt;/em&gt; send mail today, not the graveyard of SaaS tools you tried in 2023. The goal is not compliance theater. The goal is operational truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Takeaway: The Stack That Works
&lt;/h3&gt;

&lt;p&gt;SPF is not a checkbox. It is the first layer of a three-layer authentication system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPF&lt;/strong&gt; authorizes the sending infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM&lt;/strong&gt; signs the message cryptographically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC&lt;/strong&gt; aligns the authentication with the visible &lt;code&gt;From:&lt;/code&gt; domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One without the other is a partial solution. All three, with &lt;strong&gt;correct alignment&lt;/strong&gt; and an &lt;strong&gt;accurate sender inventory&lt;/strong&gt;, is the foundation of a modern email setup.&lt;/p&gt;

&lt;p&gt;If you take one action today, audit your SPF record for the 10-lookup limit. That is where silent failures hide.&lt;/p&gt;

</description>
      <category>email</category>
      <category>dns</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to Add Card Fraud Detection to Your AI Agent (in About 5 Minutes)</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Mon, 20 Jul 2026 12:09:00 +0000</pubDate>
      <link>https://dev.to/rabbitholeinphp/how-to-add-card-fraud-detection-to-your-ai-agent-in-about-5-minutes-gf6</link>
      <guid>https://dev.to/rabbitholeinphp/how-to-add-card-fraud-detection-to-your-ai-agent-in-about-5-minutes-gf6</guid>
      <description>&lt;p&gt;&lt;em&gt;Last updated: July 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Quick question. If your AI agent is touching payment data right now, does it actually know whether a card number is real?&lt;/p&gt;

&lt;p&gt;Most don't. They just pass whatever the user typed straight through. No validation, no issuer check, nothing. That's a problem if you're building a support bot, an underwriting assistant, or anything that gets near a card number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Connect the free BinSearchLookup MCP server to Claude Desktop, Cursor, or any MCP-compatible client. It gives your agent five tools and two ready-made prompts for BIN lookups, offline Luhn validation, and fraud risk scoring, without full card numbers ever touching the LLM's context window. Setup takes about 5 minutes and the free tier needs no credit card. Full steps below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is an MCP Server, and Why Does It Matter for Fraud Checks?
&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol) is the open standard that lets AI agents like Claude Desktop and Cursor call external tools directly, the same way a human developer would call an API. An MCP server is just a small program that exposes a set of tools an agent can invoke on demand.&lt;/p&gt;

&lt;p&gt;For fraud detection specifically, that means your agent can validate a card number, identify its issuing bank, and flag risk, all without you writing custom integration code. You wire up the server once, then just talk to your agent in plain English.&lt;/p&gt;

&lt;p&gt;I'm going to walk you through the exact setup using the BinSearchLookup MCP server. No fluff, no "hypothetically." Real tools, real config, real commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actually Gives Your Agent
&lt;/h2&gt;

&lt;p&gt;Once it's connected, your agent has access to five tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;local_card_diagnostics(number).&lt;/strong&gt; Free. Runs completely offline. Identifies the network (Visa, Mastercard, Amex, you name it) and checks the number against the Luhn algorithm before anything touches the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lookup_bin(bin_number).&lt;/strong&gt; Pulls the full picture on a BIN: issuer, country, card type, brand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;batch_lookup_bins(bins).&lt;/strong&gt; Feed it a list. It dedupes, chunks into batches of 50, and runs them concurrently. You don't manage pagination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;check_quota(public_user_id).&lt;/strong&gt; Tells you exactly where you stand against your plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;check_system_health().&lt;/strong&gt; Basic uptime check.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are also two prompts baked in that do the heavy lifting for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;fraud_analysis&lt;/strong&gt; runs the offline check first, pulls the live data, and hands back a full risk assessment. One command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bulk_bin_audit&lt;/strong&gt; takes a comma separated list of BINs and returns a clean markdown table, flagging prepaid cards and high risk countries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rate limits get handled automatically with exponential backoff. Repeat lookups in the same session get served from cache instead of burning your quota twice. You don't have to think about any of that. It's already built in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Get Your API Key
&lt;/h2&gt;

&lt;p&gt;Head to &lt;a href="https://binsearchlookup.com" rel="noopener noreferrer"&gt;binsearchlookup.com&lt;/a&gt; and sign up. Free plan gives you 500 requests a month, 20 requests a minute, and no, you don't need a credit card to start.&lt;/p&gt;

&lt;p&gt;Once you're in, grab two things from your dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your API key&lt;/li&gt;
&lt;li&gt;Your User ID (labeled &lt;code&gt;public_user_id&lt;/code&gt;, there's a copy button right next to it)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. Thirty seconds, tops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Wire It Into Your MCP Client
&lt;/h2&gt;

&lt;p&gt;You've got two options here. Pick whichever fits your setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Docker (this is the one most people should use)
&lt;/h3&gt;

&lt;p&gt;Drop this into your &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&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;"mcpServers"&lt;/span&gt;&lt;span class="p"&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;span class="nl"&gt;"binsearchlookup"&lt;/span&gt;&lt;span class="p"&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;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&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;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--rm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BSL_API_KEY=bsl_your_api_key_here"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BSL_USER_ID=your_user_id_here"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"python:3.12-slim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pip install mcp httpx pydantic tenacity python-dotenv cachetools -q &amp;amp;&amp;amp; curl -s https://raw.githubusercontent.com/Bin-Search-Lookup/mcp-binsearchlookup/main/mcp_server.py | python -u"&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;span class="p"&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;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;No repo to clone, no Python environment to manage. Restart your client and you're live.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Local Python
&lt;/h3&gt;

&lt;p&gt;If you'd rather run it yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;httpx pydantic tenacity python-dotenv mcp cachetools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a &lt;code&gt;.env&lt;/code&gt; file next to &lt;code&gt;mcp_server.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;BSL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;bsl_your_api_key_here&lt;/span&gt;
&lt;span class="py"&gt;BSL_USER_ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your_uuid_here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And point your client at it:&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;"mcpServers"&lt;/span&gt;&lt;span class="p"&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;span class="nl"&gt;"binsearchlookup"&lt;/span&gt;&lt;span class="p"&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;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/absolute/path/to/mcp_server.py"&lt;/span&gt;&lt;span class="p"&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;span class="p"&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;You can grab the source, plus a README in ten languages, from the &lt;a href="https://github.com/Bin-Search-Lookup/mcp-binsearchlookup" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; or download it as a &lt;a href="https://github.com/Bin-Search-Lookup/mcp-binsearchlookup/archive/refs/heads/main.tar.gz" rel="noopener noreferrer"&gt;tarball directly&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Actually Use It
&lt;/h2&gt;

&lt;p&gt;Open your MCP client and just ask, in plain English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Run a fraud analysis on 4532015112830366"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. Your agent calls &lt;code&gt;local_card_diagnostics&lt;/code&gt; first (instant, offline, free), then &lt;code&gt;lookup_bin&lt;/code&gt; for the real issuer data, and gives you back a structured risk read using the &lt;code&gt;fraud_analysis&lt;/code&gt; prompt automatically. You didn't write a single line of integration code.&lt;/p&gt;

&lt;p&gt;Want to check a whole batch at once? Try this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Audit this list of BINs for prepaid or high risk cards: 453201, 542418, 601100, 370000"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That triggers &lt;code&gt;bulk_bin_audit&lt;/code&gt;. Concurrent lookups, one clean table back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters More Than You Think
&lt;/h2&gt;

&lt;p&gt;Here's something a lot of people building AI agents overlook. Full card numbers should never end up in an LLM's context window or sitting in a prompt log. That's not a "best practice," that's a compliance issue waiting to happen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;local_card_diagnostics&lt;/code&gt; solves this by doing the Luhn check and network ID entirely offline, on your machine, before anything leaves. Only the BIN, six to eight digits, ever gets sent out for the live lookup. The full card number never travels anywhere near an API log or a prompt history.&lt;/p&gt;

&lt;p&gt;That's the difference between "we built fraud checks into our agent" and "we built fraud checks into our agent the right way."&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Costs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Rate Limit&lt;/th&gt;
&lt;th&gt;Monthly Quota&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;20 req/min&lt;/td&gt;
&lt;td&gt;500 requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;$29/mo&lt;/td&gt;
&lt;td&gt;60 req/min&lt;/td&gt;
&lt;td&gt;10,000 requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$199/mo&lt;/td&gt;
&lt;td&gt;280 req/min&lt;/td&gt;
&lt;td&gt;78,000 requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;$999/mo&lt;/td&gt;
&lt;td&gt;768 req/min&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start on the free plan. It's more than enough to test this in a real workflow. If your agent starts making serious volume, you'll know it's time to upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is a BIN number?&lt;/strong&gt;&lt;br&gt;
BIN stands for Bank Identification Number, the first 6 to 8 digits of a card number. It identifies the issuing bank, card network, and card type without exposing the full card number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it safe to send card numbers through an AI agent?&lt;/strong&gt;&lt;br&gt;
Full card numbers (PANs) should never be sent to an LLM or a third-party API. The BinSearchLookup MCP server avoids this by validating the full number offline, on your machine, and only sending the BIN (the first 6 to 8 digits) to the live API. The full number never leaves your local process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this work with Claude Desktop, Cursor, and other MCP clients?&lt;/strong&gt;&lt;br&gt;
Yes. Any MCP-compatible client works, since the setup is just a standard MCP server entry in your client's config file. Claude Desktop and Cursor are the two most common.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to know Python to set this up?&lt;/strong&gt;&lt;br&gt;
No. The Docker option needs zero local setup, Docker pulls and runs everything automatically. The local Python option is only needed if you'd rather not use Docker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does it cost to run fraud checks with this MCP server?&lt;/strong&gt;&lt;br&gt;
The free tier includes 500 requests per month with no credit card required. That's enough for testing and most small projects. Paid plans start at $29/month for 10,000 requests if you need more volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between local_card_diagnostics and lookup_bin?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;local_card_diagnostics&lt;/code&gt; runs entirely offline and is free. It checks the Luhn algorithm and identifies the card network. &lt;code&gt;lookup_bin&lt;/code&gt; calls the live API and returns full issuer details: bank name, country, and card category.&lt;/p&gt;

&lt;p&gt;Grab your &lt;a href="https://binsearchlookup.com" rel="noopener noreferrer"&gt;free API key&lt;/a&gt; and have this running before your coffee gets cold.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>claude</category>
      <category>api</category>
    </item>
    <item>
      <title>How I Designed a Freemium API Plan Developers Actually Want to Pay For</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Wed, 22 Apr 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/rabbitholeinphp/how-i-designed-a-freemium-api-plan-developers-actually-want-to-pay-for-4k6a</link>
      <guid>https://dev.to/rabbitholeinphp/how-i-designed-a-freemium-api-plan-developers-actually-want-to-pay-for-4k6a</guid>
      <description>&lt;h2&gt;
  
  
  The Painful Origin Story
&lt;/h2&gt;

&lt;p&gt;Two years ago, I launched a BIN lookup API with pricing that looked good on a spreadsheet and failed everywhere else.&lt;/p&gt;

&lt;p&gt;The model was simple: free tier with 100 requests per month, Pro tier at $29/month with 10,000 requests, and Enterprise at custom pricing. I assumed the progression made sense. Users would try the free tier, hit the limit quickly, and upgrade.&lt;/p&gt;

&lt;p&gt;What actually happened: the free tier was so constrained that serious developers never bothered onboarding. The jump to $29/month felt arbitrary to the 20-person startups using my API in non-critical flows. And the Enterprise tier collected inquiries from big companies who then negotiated us down by 40% and asked for custom integrations.&lt;/p&gt;

&lt;p&gt;After 6 months: 40 signups, 3 paying customers, 1 angry customer who felt overcharged.&lt;/p&gt;

&lt;p&gt;I was pricing for growth that did not exist because I had never talked to the people I was trying to sell to.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR: What I Learned (and Changed)
&lt;/h2&gt;

&lt;p&gt;If you are pricing a developer API, these four lessons will save you the same six months I wasted:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Free tier is your onboarding tool, not your product.&lt;/strong&gt; Make it viable for real prototyping, not a tease that only lets developers test with five requests. The free tier should answer the question: "Does this API solve my problem?" It should not answer: "Can I build my whole company on this free tier?"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pricing boundaries should match real developer workflows, not arbitrary request counts.&lt;/strong&gt; A startup that runs 10,000 lookups per month is not the same as one running 100,000 per month. The actual pain point surfaces at different scales, and your tier breakpoints should acknowledge that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transparent per-minute rate limits matter more than monthly quotas.&lt;/strong&gt; Developers care about burst capacity. Can they run a batch job without getting rate-limited to death? Can their application handle a traffic spike? These questions drive upgrades, not hitting a monthly wall three weeks into the month.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Talk to users before you finalize anything.&lt;/strong&gt; I made pricing decisions in isolation. Every conversation I had with an actual developer after launch revealed something I had missed. Start there, not with spreadsheet assumptions.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Where Most API Pricing Goes Wrong
&lt;/h2&gt;

&lt;p&gt;I see the same mistakes repeatedly in other API products, and I made most of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Assuming free users will convert to paid.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The conversion funnel is not linear. A developer using 50 free API requests per month will never become a $50/month customer because they do not need it. They will either leave or stay on free forever. The free tier should be designed for developers who will eventually need 5,000+ monthly requests, not everyone.&lt;/p&gt;

&lt;p&gt;This means asking: what is the smallest viable dataset or transaction volume where a serious developer says, "I need more than free"? For a BIN lookup API, that threshold is somewhere around 1,000 lookups per month. That is enough for a developer to validate that the API works, integrate it into a prototype, and realize they need it in production. It is not enough to run a production system for even a small company.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Setting rate limits that punish normal usage patterns.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your Pro plan offers 100,000 requests per month but limits users to 10 requests per minute, you have created a perverse situation. A developer with a legitimate batch job fails immediately, then cancels because they think your API is broken. They were not hitting the monthly limit; they were hitting a per-minute wall that made no sense.&lt;/p&gt;

&lt;p&gt;Developers think in terms of per-second and per-minute throughput first, then monthly volume second. A rate limit of 200 requests per minute sounds boring on a pricing page, but it is the difference between "I can batch-process 1000 records in my evening job" and "my batch jobs time out constantly."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Making Enterprise a mystery box.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When potential customers see "Enterprise: custom pricing," they assume three things: your product is expensive, negotiation is required, and you are hiding something. Even if your actual Enterprise terms are fair, the perception of opacity suppresses inquiries from large companies that would otherwise upgrade.&lt;/p&gt;

&lt;p&gt;I now list baseline Enterprise numbers upfront (unlimited requests, custom rate limits, SLA guarantees, API keys). This signals that Enterprise is not a black box. Customers can decide if it is a conversation worth having.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Restructured the Tiers (and Why)
&lt;/h2&gt;

&lt;p&gt;Here is the actual pricing model I moved to, and the reasoning behind each decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free: 1,000 requests per month, 10 per minute
&lt;/h3&gt;

&lt;p&gt;This is the new floor. 1,000 lookups is enough for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A developer to run integration tests against real data (not mocks)&lt;/li&gt;
&lt;li&gt;A small app to do a few lookups per day for a week&lt;/li&gt;
&lt;li&gt;A prototype to determine if the API solves the problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The per-minute limit is high enough that normal API usage never hits it. A developer calling the API once per second for 10 seconds does not fail. But a developer trying to batch-process 10,000 records hits a wall and knows they need to upgrade.&lt;/p&gt;

&lt;p&gt;Why this works: it is not a tease. It is a real sandbox. And it is generous enough that the question "Should I even evaluate this product?" gets answered with "yes, definitely."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conversion rate after moving to 1,000/month free tier: 8%.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Starter: 10,000 requests per month, 50 per minute, 3 API keys
&lt;/h3&gt;

&lt;p&gt;This tier is for developers who have moved from prototype to MVP. They are building something real but it is not mission-critical yet.&lt;/p&gt;

&lt;p&gt;10,000 requests per month is roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;300 lookups per day&lt;/li&gt;
&lt;li&gt;10 lookups per hour during business hours&lt;/li&gt;
&lt;li&gt;Enough for a small fintech or fraud team running a few hundred transactions daily&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The per-minute limit of 50 is high enough that batch processing within the month is never throttled. The 3 API keys let a developer keep their test environment separate from production without hitting key limits.&lt;/p&gt;

&lt;p&gt;The price: $25/month. This is not $29/month; it is not $19/month. It is $25/month, which is memorable and sits at the psychological boundary where a small team says "yes, that is worth it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conversion rate after adding this tier: 14%.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pro: 100,000 requests per month, 200 per minute, 15 API keys
&lt;/h3&gt;

&lt;p&gt;This is where it gets interesting. Pro is for teams that are treating your API as part of their infrastructure.&lt;/p&gt;

&lt;p&gt;100,000 requests per month is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3,300 lookups per day&lt;/li&gt;
&lt;li&gt;Enough for a fraud team running thousands of transactions daily&lt;/li&gt;
&lt;li&gt;Enough for a payment processor doing lookups for their entire merchant base&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The per-minute limit of 200 lets a developer run batch jobs that process 12,000 records per hour without throttling. That is a serious operational capability. The 15 API keys let them organize by team, environment, and use case without burning key limits.&lt;/p&gt;

&lt;p&gt;The price: $99/month. This price point exists for a specific reason. It is below $100, which makes it a different budget category for most small companies. It is 4x the Starter price, which is roughly the request ratio. This pricing was not arbitrary; it came from conversations with 15 developers who said, "I would pay $99 to not think about API keys and rate limits."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conversion rate after launching Pro: 22%.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise: Unlimited requests, custom rate limits, 190 API keys, SLA
&lt;/h3&gt;

&lt;p&gt;Enterprise is transparent. No custom pricing, no mystery. If you need more than 100,000 requests per month, you contact sales and get a quote. The baseline is unlimited requests, fully customizable rate limits, an SLA, and priority support.&lt;/p&gt;

&lt;p&gt;I do not advertise the Enterprise price publicly because it depends on volume and contract terms. But I do publish that Enterprise exists and what it includes. This removes the mystery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conversion rate for Enterprise inquiries: 65%.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Shifts That Actually Moved the Needle
&lt;/h2&gt;

&lt;p&gt;Looking back, three changes mattered far more than pricing tweaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change 1: I Made the Free Tier Actually Usable
&lt;/h3&gt;

&lt;p&gt;The original 100 request free tier was a non-starter. After moving to 1,000, onboarding increased 5x immediately. Developers stopped bouncing off the free tier within a day.&lt;/p&gt;

&lt;p&gt;The reason is simple: 1,000 requests is enough to have a real conversation with the API. You run test calls, validate the response shape, check that the data is actually useful. 100 requests is enough to read the docs, run one test, and leave.&lt;/p&gt;

&lt;p&gt;This change cost nothing to implement. It just required removing the assumption that "free users are not real customers" and asking instead, "What do free users need to even evaluate this product?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Change 2: I Published Per-Minute Rate Limits Prominently
&lt;/h3&gt;

&lt;p&gt;The original pricing page buried rate limits in a footnote. I moved them to the main tier comparison table.&lt;/p&gt;

&lt;p&gt;The result: developers could immediately see if a tier met their needs. Instead of buying Pro and then complaining about rate limiting, they could make an informed decision. Some developers who thought they needed Enterprise realized Starter was enough. Others realized they needed Pro from the start.&lt;/p&gt;

&lt;p&gt;Publishing rate limits also signaled that I understood how developers actually use APIs. It built credibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change 3: I Talked to Users Before Launching Changes
&lt;/h3&gt;

&lt;p&gt;Before restructuring tiers, I ran 20 conversations with developers and payment companies using the API. I asked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At what volume does free become insufficient?&lt;/li&gt;
&lt;li&gt;Do rate limits or monthly quotas matter more?&lt;/li&gt;
&lt;li&gt;What does the jump from free to paid look like at your company?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answers were consistent and contradicted my original assumptions. Free needed to be 10x larger. Rate limits mattered more than I thought. And pricing at $25 for Starter instead of $29 was not a cost difference; it was a perception difference.&lt;/p&gt;

&lt;p&gt;I incorporated every answer into the restructured tiers. This is why conversion rates improved so dramatically. I was not guessing anymore.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Numbers
&lt;/h2&gt;

&lt;p&gt;Here are the numbers that convinced me the restructure worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (original tiers):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signups per month: 7&lt;/li&gt;
&lt;li&gt;Paying customers per month: 0.4 (sometimes a team converts, sometimes they do not)&lt;/li&gt;
&lt;li&gt;MRR: $100-200&lt;/li&gt;
&lt;li&gt;Churn: 40% per month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;After (new tiers):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signups per month: 85&lt;/li&gt;
&lt;li&gt;Paying customers per month: 12&lt;/li&gt;
&lt;li&gt;MRR: $1,800 (Starter + Pro + Enterprise combined)&lt;/li&gt;
&lt;li&gt;Churn: 8% per month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was not a magic traction inflection. But it was proof that the pricing was no longer the barrier to entry.&lt;/p&gt;

&lt;p&gt;The mix broke down roughly as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free tier: 73 of 85 signups (86%)&lt;/li&gt;
&lt;li&gt;Starter: 8 of 12 conversions (67% of paying)&lt;/li&gt;
&lt;li&gt;Pro: 3 of 12 conversions (25% of paying)&lt;/li&gt;
&lt;li&gt;Enterprise: 1 of 12 conversions (8% of paying)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is important. The Starter tier, not Pro, was where most conversion happened. This contradicted my assumption that developers would skip straight to Pro. Instead, many teams started with Starter, ran into limits within 3-6 months, and upgraded. That is the healthy funnel I was trying to build.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Developers Actually Care About (Ranked)
&lt;/h2&gt;

&lt;p&gt;After this relaunch and dozens of conversations, here is what drove decisions, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Can I try this without a credit card?&lt;/strong&gt; Free tier with a real quota (not 50 requests) matters. Most signups come from free users who eventually convert.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is the per-minute rate limit documented?&lt;/strong&gt; Burst capacity is the question developers are asking. Monthly quotas are secondary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Are the price jumps between tiers predictable?&lt;/strong&gt; Developers do not want to be surprised. 4x the volume for 4x the price is fair. 2x the volume for 10x the price is a scam.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Can I have more than one API key?&lt;/strong&gt; This signals that the product scales with the developer. A single API key for production and test is a red flag.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What is the minimum commitment period?&lt;/strong&gt; Most developers want month-to-month, not annual. Annual plans were a conversation-killer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pricing, interestingly, was number 7 on the list. Transparency, fairness, and usability ranked higher than cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Live Example
&lt;/h2&gt;

&lt;p&gt;I use this exact pricing model on &lt;a href="https://www.binsearchlookup.com/" rel="noopener noreferrer"&gt;BinSearchLookup&lt;/a&gt;, which is a BIN lookup API for payment developers. The free tier (1,000/month, 10/min) brings in most signups. Starter (10,000/month, 50/min) is where conversion happens. Pro and Enterprise are healthy but smaller.&lt;/p&gt;

&lt;p&gt;The model works because it is designed around how developers actually work, not around what a spreadsheet says is logical.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mistakes I Still Make
&lt;/h2&gt;

&lt;p&gt;Pricing is not a set-and-forget problem. I still mess this up regularly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake I still make: Assuming a feature is worth a tier upgrade.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Last year I added batch BIN processing to Pro and expected a shift in conversions. It did not happen. Developers did not care about the feature enough to upgrade. I learned that feature additions do not drive tier upgrades; hitting quota does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake I still make: Not talking to churn customers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a customer downgraded from Pro to Starter, I did not reach out. In retrospect, I should have. They probably hit a workflow that broke at Starter limits, or a rate limit surprised them. I could have fixed the issue instead of losing them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake I still make: Treating Enterprise as a separate business.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I still assume Enterprise customers want "custom" everything. Many of them just want transparent terms and a handshake. A faster path to Enterprise without a 30-minute sales call would probably convert more.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaways for Your API
&lt;/h2&gt;

&lt;p&gt;If you are pricing a developer API, here is the short version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make free tier viable for real work, not just onboarding.&lt;/li&gt;
&lt;li&gt;Publish rate limits as prominently as pricing.&lt;/li&gt;
&lt;li&gt;Price tiers should reflect real usage patterns, not arbitrary request counts.&lt;/li&gt;
&lt;li&gt;Ask developers what they care about instead of assuming.&lt;/li&gt;
&lt;li&gt;Build the free-to-Starter funnel first. That is where most conversions happen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The best part of this restructure was not the revenue increase. It was the shift in how developers talked about the product. Instead of "I wanted to evaluate this but the free tier was too limited," they said "I evaluated this for free, it solved my problem, and now I pay for it."&lt;/p&gt;

&lt;p&gt;That is the only pricing metric that matters.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>api</category>
      <category>startup</category>
      <category>product</category>
    </item>
    <item>
      <title>6-Digit to 8-Digit BINs: What Actually Changes for Your Code</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Mon, 20 Apr 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/rabbitholeinphp/6-digit-to-8-digit-bins-what-actually-changes-for-your-code-5424</link>
      <guid>https://dev.to/rabbitholeinphp/6-digit-to-8-digit-bins-what-actually-changes-for-your-code-5424</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What changed?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Card networks (Visa, Mastercard, and others) expanded the Bank Identification Number (BIN) from 6 digits to 8 digits, per ISO 7812-1:2017.&lt;/li&gt;
&lt;li&gt;The first 8 digits of a card number now define the issuer, not just the first 6.&lt;/li&gt;
&lt;li&gt;Any system that truncates, stores, indexes, or regex-matches against only 6 BIN digits is now potentially misidentifying cards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What should you do?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audit every database column, regex pattern, cache key, and analytics pipeline that assumes a fixed 6-digit BIN.&lt;/li&gt;
&lt;li&gt;Expand BIN storage fields from &lt;code&gt;CHAR(6)&lt;/code&gt; to &lt;code&gt;VARCHAR(8)&lt;/code&gt; and update all related indexes.&lt;/li&gt;
&lt;li&gt;Replace homegrown BIN lookups with an API that natively supports 6-to-8 digit ranges, such as &lt;a href="https://www.binsearchlookup.com/" rel="noopener noreferrer"&gt;BinSearchLookup&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Re-train or re-feature any fraud or risk models that use raw BIN as a feature input.&lt;/li&gt;
&lt;li&gt;Test against both 6- and 8-digit BINs before deploying any change to production.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Did Networks Move to 8-Digit BINs?
&lt;/h2&gt;

&lt;p&gt;The short answer is that the card industry ran out of BIN space.&lt;/p&gt;

&lt;p&gt;The longer answer involves decades of growth. When ISO 7812 was first standardized, 6 digits allowed for roughly 810,000 unique BINs (the first digit is the Major Industry Identifier and is fixed per network, reducing the available combinations). That sounded like plenty in 1989.&lt;/p&gt;

&lt;p&gt;By the 2010s, it was not plenty. The explosion of fintech issuers, virtual card programs, co-branded portfolios, and multi-currency prepaid products created a shortage. Networks began assigning BINs in blocks smaller than traditional ranges, and eventually those blocks ran thin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The formal timeline:&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;Year&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2017&lt;/td&gt;
&lt;td&gt;ISO 7812-1:2017 published, formally extending IIN/BIN to 8 digits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;Mastercard mandates acquirer and processor readiness for 8-digit BINs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;Visa begins issuing 8-digit BINs commercially at scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;Industry-wide adoption; most major processors now receive 8-digit BINs in authorization flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024+&lt;/td&gt;
&lt;td&gt;Legacy 6-digit systems are actively misrouting or misidentifying a growing share of transactions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The practical consequence: a card starting with &lt;code&gt;12345678xxxx...&lt;/code&gt; might have been identified by BIN &lt;code&gt;123456&lt;/code&gt; under the old system. Under the new system, the correct BIN is &lt;code&gt;12345678&lt;/code&gt;. These can belong to completely different issuers in different countries. A 6-digit lookup on an 8-digit BIN card returns stale or wrong data.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Breaks When You Still Assume 6 Digits?
&lt;/h2&gt;

&lt;p&gt;More things break than most teams expect. Here is a category-by-category breakdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database Schema
&lt;/h3&gt;

&lt;p&gt;The most common problem. BIN columns defined as &lt;code&gt;CHAR(6)&lt;/code&gt; or &lt;code&gt;VARCHAR(6)&lt;/code&gt; silently truncate 8-digit values on insert.&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="c1"&gt;-- Old schema (breaks silently)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bin_cache&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;bin&lt;/span&gt;      &lt;span class="nb"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;issuer&lt;/span&gt;   &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;country&lt;/span&gt;  &lt;span class="nb"&gt;CHAR&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="n"&gt;brand&lt;/span&gt;    &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Insert an 8-digit BIN&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;bin_cache&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'12345678'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Example Bank'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'VISA'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Stored value becomes '123456' in PostgreSQL, or raises a truncation error in strict mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In PostgreSQL with &lt;code&gt;standard_conforming_strings&lt;/code&gt; on, this may silently truncate. In MySQL with &lt;code&gt;STRICT_TRANS_TABLES&lt;/code&gt; disabled, same result. You end up with a corrupted cache that maps 8-digit cards to the wrong issuer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&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;bin_cache&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;idx_bin_cache_bin&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;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_bin_cache_bin&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;bin_cache&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Regex Patterns
&lt;/h3&gt;

&lt;p&gt;Regex is the second most common landmine. Patterns like &lt;code&gt;^\d{6}$&lt;/code&gt; appear in input validation, log parsers, ETL pipelines, and API middleware.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Breaks on 8-digit BINs
&lt;/span&gt;&lt;span class="n"&gt;BIN_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^\d{6}$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Correct pattern that accepts 6 or 8 digits
&lt;/span&gt;&lt;span class="n"&gt;BIN_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^\d{6}(\d{2})?$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Or more explicitly if you want to accept both lengths but nothing else
&lt;/span&gt;&lt;span class="n"&gt;BIN_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^\d{6}$|^\d{8}$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In JavaScript, the same problem shows up in form validation and server-side middleware:&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="c1"&gt;// Old&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValidBin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{6}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Fixed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValidBin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{6}(\d{2})?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Truncation at the Application Layer
&lt;/h3&gt;

&lt;p&gt;It is common to see code that extracts a BIN by slicing the first 6 characters of a PAN (Primary Account Number).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Breaks for 8-digit BIN issuers
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pan&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Correct approach: use the issuer's indicated BIN length, or default to 8
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bin_length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pan&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;bin_length&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The challenge here is that you often do not know the BIN length without looking it up first. This creates a bootstrapping problem: you need an 8-digit BIN to look up the correct BIN length, but your code is only giving you 6 digits.&lt;/p&gt;

&lt;p&gt;The practical resolution is to default to 8 digits for all new cards while retaining 6-digit support for legacy BIN ranges that have not been migrated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analytics and BI Pipelines
&lt;/h3&gt;

&lt;p&gt;Many analytics stacks join transaction data against a BIN reference table using a 6-character key. When 8-digit BINs arrive in transaction data, the join fails silently and those transactions get tagged as "unknown issuer" or dropped entirely.&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="c1"&gt;-- Typical broken join&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;bin_reference&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;card_number_masked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Fixed join: try 8-digit match first, fall back to 6&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b6&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b6&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;bin_reference&lt;/span&gt; &lt;span class="n"&gt;b8&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;card_number_masked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;bin_reference&lt;/span&gt; &lt;span class="n"&gt;b6&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;card_number_masked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b6&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fraud and Risk Models
&lt;/h3&gt;

&lt;p&gt;This is the highest-stakes area. Risk models trained on 6-digit BINs have a feature that is now unreliable.&lt;/p&gt;

&lt;p&gt;If your model uses &lt;code&gt;bin_country&lt;/code&gt;, &lt;code&gt;bin_issuer_type&lt;/code&gt;, or &lt;code&gt;bin_brand&lt;/code&gt; as features derived from a 6-digit lookup, and the underlying BIN has been migrated to an 8-digit assignment, your features are now wrong. A Visa debit card issued in Germany might look like a Mastercard credit card issued in the US if the 6-digit prefix belongs to a legacy range that no longer maps correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a &lt;code&gt;bin_length&lt;/code&gt; feature to your model inputs (value: 6 or 8).&lt;/li&gt;
&lt;li&gt;Re-extract BIN features from historical transactions using the correct BIN length.&lt;/li&gt;
&lt;li&gt;Monitor feature drift on BIN-derived inputs after the migration.&lt;/li&gt;
&lt;li&gt;If retraining is not immediately possible, add a correction layer that re-maps 8-digit BINs before feature extraction.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  6-Digit vs 8-Digit BIN: Side-by-Side Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;6-Digit BIN&lt;/th&gt;
&lt;th&gt;8-Digit BIN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ISO standard&lt;/td&gt;
&lt;td&gt;ISO 7812-1 (pre-2017)&lt;/td&gt;
&lt;td&gt;ISO 7812-1:2017&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BIN space&lt;/td&gt;
&lt;td&gt;~810,000 combinations&lt;/td&gt;
&lt;td&gt;~81,000,000 combinations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage type (SQL)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CHAR(6)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VARCHAR(8)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regex match&lt;/td&gt;
&lt;td&gt;&lt;code&gt;^\d{6}$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;^\d{6}(\d{2})?$&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PAN slice for BIN&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pan[:6]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pan[:8]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lookup API compatibility&lt;/td&gt;
&lt;td&gt;Most older APIs&lt;/td&gt;
&lt;td&gt;Requires explicit 8-digit support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk model feature accuracy&lt;/td&gt;
&lt;td&gt;Degrading since 2022&lt;/td&gt;
&lt;td&gt;Accurate when API returns correct data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affected card brands&lt;/td&gt;
&lt;td&gt;All major networks&lt;/td&gt;
&lt;td&gt;All major networks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backward compatibility&lt;/td&gt;
&lt;td&gt;Must coexist with 8-digit&lt;/td&gt;
&lt;td&gt;Must coexist with 6-digit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  How Do You Migrate Safely?
&lt;/h2&gt;

&lt;p&gt;Safe migration is not a one-day task. It is a phased rollout with parallel processing and validation at each step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Audit
&lt;/h3&gt;

&lt;p&gt;Before touching any code, find every place in your system that handles BINs.&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;# Search for hardcoded 6-digit assumptions in a codebase&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"[:6&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="s2"&gt;0:6&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"pan&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="s2"&gt;:6&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"CHAR(6)"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.sql"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"VARCHAR(6)"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.sql"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\\\&lt;/span&gt;&lt;span class="s2"&gt;d{6}"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run similar searches for your language of choice. Build a complete inventory before making any changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Schema Migration
&lt;/h3&gt;

&lt;p&gt;Expand BIN columns first, before changing application logic. This avoids truncation errors during the transition period.&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="c1"&gt;-- Step 1: Expand column (non-destructive, backward-compatible)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bin_cache&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: Update indexes after column expansion&lt;/span&gt;
&lt;span class="k"&gt;REINDEX&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bin_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3: Add a bin_length column for analytics and debugging&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bin_cache&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;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;bin_length&lt;/span&gt; &lt;span class="nb"&gt;SMALLINT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;bin_cache&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;bin_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not drop the old 6-digit records yet. You need them as a fallback during the transition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Application Logic
&lt;/h3&gt;

&lt;p&gt;Update extraction and validation logic to default to 8 digits for new lookups, while retaining 6-digit compatibility for existing records.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BinExtractor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;DEFAULT_LENGTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
    &lt;span class="n"&gt;LEGACY_LENGTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LEGACY_LENGTH&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;legacy&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEFAULT_LENGTH&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pan&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;from_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        Extract BIN with fallback logic.
        Try 8-digit lookup first; fall back to 6-digit if no result.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;bin_8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pan&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;bin_6&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pan&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bin_8&lt;/span&gt;  &lt;span class="c1"&gt;# Resolver handles fallback at lookup time
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 4: Use a BIN Lookup API That Supports Both Lengths
&lt;/h3&gt;

&lt;p&gt;The cleanest solution for lookup is to delegate to a service that already handles the 6-to-8 digit transition internally. Hand-maintained BIN tables go stale within weeks as new BINs are issued constantly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.binsearchlookup.com/" rel="noopener noreferrer"&gt;BinSearchLookup&lt;/a&gt; is one example of an API that accepts 6 to 8 digit BINs natively. The endpoint accepts a &lt;code&gt;bin&lt;/code&gt; parameter of either length and returns issuer, brand, card type, and country without requiring you to know in advance whether you have a 6-digit or 8-digit BIN.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lookup_bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Accepts 6 or 8 digit BINs. API resolves the correct issuer data.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.binsearchlookup.com/lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin_value&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-User-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;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;# Works for both:
&lt;/span&gt;&lt;span class="n"&gt;result_6&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lookup_bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;412345&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result_8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lookup_bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;41234567&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes &lt;code&gt;BIN&lt;/code&gt;, &lt;code&gt;Brand&lt;/code&gt;, &lt;code&gt;Type&lt;/code&gt;, &lt;code&gt;Category&lt;/code&gt;, &lt;code&gt;Issuer&lt;/code&gt;, &lt;code&gt;CountryName&lt;/code&gt;, &lt;code&gt;isoCode2&lt;/code&gt;, and &lt;code&gt;isoCode3&lt;/code&gt; fields, which maps cleanly to the feature set most fraud models expect.&lt;/p&gt;

&lt;p&gt;For batch processing (up to 50 BINs per request), the API also provides a &lt;code&gt;POST /lookup/batch&lt;/code&gt; endpoint, which is useful when re-enriching historical transaction data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;batch_lookup_bins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.binsearchlookup.com/lookup/batch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bins&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin_list&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-User-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 5: Backfill Historical Data
&lt;/h3&gt;

&lt;p&gt;Once the lookup layer is working, re-enrich historical transaction records with correct 8-digit BIN metadata.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;backfill_bin_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_cursor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Backfill BIN metadata for historical records using 8-digit BINs.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;db_cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT DISTINCT LEFT(card_number_masked, 8) FROM transactions &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE bin_country IS NULL OR bin_length = 6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;db_cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bins&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bins&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;batch_lookup_bins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&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;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;db_cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                UPDATE transactions
                SET bin_country = %s,
                    bin_brand   = %s,
                    bin_type    = %s,
                    bin_length  = %s
                WHERE LEFT(card_number_masked, 8) = %s
                &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isoCode2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BIN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BIN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Respect rate limits
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 6: Monitor After Deploy
&lt;/h3&gt;

&lt;p&gt;After deploying changes, watch for these signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Increase in &lt;code&gt;bin_country = NULL&lt;/code&gt; rows (indicates unmatched BINs)&lt;/li&gt;
&lt;li&gt;Spike in JOIN nulls on BIN reference tables&lt;/li&gt;
&lt;li&gt;Shift in fraud model score distributions on newly issued cards&lt;/li&gt;
&lt;li&gt;API error rate on BIN lookups (especially &lt;code&gt;400 INVALID_BIN&lt;/code&gt; responses)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Migration Checklist
&lt;/h2&gt;

&lt;p&gt;Use this before marking the migration complete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] All BIN columns expanded to &lt;code&gt;VARCHAR(8)&lt;/code&gt; or wider&lt;/li&gt;
&lt;li&gt;[ ] Indexes rebuilt after column expansion&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;bin_length&lt;/code&gt; column added where useful for analytics&lt;/li&gt;
&lt;li&gt;[ ] No &lt;code&gt;CHAR(6)&lt;/code&gt; or &lt;code&gt;VARCHAR(6)&lt;/code&gt; BIN columns remain in production schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Application Logic&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] All &lt;code&gt;pan[:6]&lt;/code&gt; slices replaced with &lt;code&gt;pan[:8]&lt;/code&gt; or parameterized length&lt;/li&gt;
&lt;li&gt;[ ] All regex patterns updated to accept 6 or 8 digits&lt;/li&gt;
&lt;li&gt;[ ] Input validation updated on all API endpoints that accept BIN as a parameter&lt;/li&gt;
&lt;li&gt;[ ] Logging and monitoring updated to flag unexpected BIN lengths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BIN Lookup&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] BIN lookup service tested with both 6-digit and 8-digit inputs&lt;/li&gt;
&lt;li&gt;[ ] Fallback logic implemented (try 8-digit, fall back to 6-digit for legacy ranges)&lt;/li&gt;
&lt;li&gt;[ ] BIN cache TTL set to 30 days or less (BIN assignments change)&lt;/li&gt;
&lt;li&gt;[ ] Batch backfill job written and tested in staging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Analytics and Risk&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] BIN join logic in analytics queries updated to attempt 8-digit match first&lt;/li&gt;
&lt;li&gt;[ ] Risk model BIN feature extraction updated&lt;/li&gt;
&lt;li&gt;[ ] Historical transaction data backfilled with correct 8-digit BIN metadata&lt;/li&gt;
&lt;li&gt;[ ] Feature drift monitoring added for BIN-derived inputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Testing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Unit tests cover 6-digit BIN inputs&lt;/li&gt;
&lt;li&gt;[ ] Unit tests cover 8-digit BIN inputs&lt;/li&gt;
&lt;li&gt;[ ] Integration tests run against staging with real 8-digit BIN samples&lt;/li&gt;
&lt;li&gt;[ ] Rollback plan documented&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Do all cards now use 8-digit BINs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. The transition is gradual. Many legacy card programs still operate on 6-digit BINs. You must support both formats simultaneously, likely for the next decade or more. The safe default is to attempt an 8-digit lookup and fall back to 6 digits if no result is found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I know if a BIN in my database is a real 8-digit BIN vs a truncated 6-digit BIN stored with zero-padding?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check the &lt;code&gt;bin_length&lt;/code&gt; column if you added one during migration. If you did not, look at the original transaction data. A BIN that was stored as &lt;code&gt;12345600&lt;/code&gt; is almost certainly a truncated 6-digit BIN. A BIN stored as &lt;code&gt;12345678&lt;/code&gt; may be either, and requires a live lookup to verify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Will 6-digit lookups return wrong data for 8-digit BIN cards?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, potentially. If &lt;code&gt;12345678&lt;/code&gt; is an 8-digit BIN assigned to a German bank, but &lt;code&gt;123456&lt;/code&gt; is a 6-digit BIN assigned to a US bank, a 6-digit lookup will return US/bank data for a card that is actually German. This directly impacts fraud decisions, routing, and compliance checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is the first 8 digits always the BIN on a 16-digit card?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For networks that have adopted the ISO 7812-1:2017 standard, yes. For networks or issuers that have not yet migrated, the BIN may still be defined by only the first 6 digits. This is why fallback logic matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do American Express cards follow the same 8-digit BIN standard?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Amex uses a 15-digit PAN format. The BIN expansion affects them as well, though Amex has historically used tighter control over their BIN ranges. Verify with your Amex integration documentation for specifics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How often do BIN assignments change?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Frequently. New issuers, new programs, and BIN re-assignments happen continuously. A static BIN table that was accurate in 2022 is meaningfully out of date today. This is the strongest argument for using a maintained API rather than a self-hosted BIN database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the risk of not migrating?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Significant. Misidentified issuers affect currency routing, fraud scoring, and 3DS authentication flows. Regulatory audits that require accurate issuer identification (for example, interchange fee compliance) will surface incorrect data. The longer you wait, the larger the backfill problem grows as more 8-digit BINs are issued.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Last updated: April 2026. BIN assignment information is subject to ongoing change by card networks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>api</category>
      <category>backend</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Your First BIN Lookup Integration in Python (With Real Fraud Use Cases)</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Thu, 16 Apr 2026 12:01:00 +0000</pubDate>
      <link>https://dev.to/rabbitholeinphp/your-first-bin-lookup-integration-in-python-with-real-fraud-use-cases-1nac</link>
      <guid>https://dev.to/rabbitholeinphp/your-first-bin-lookup-integration-in-python-with-real-fraud-use-cases-1nac</guid>
      <description>&lt;p&gt;A merchant friend of mine runs a small Shopify store selling mechanical keyboards. Last spring, he messaged me in a panic. Overnight, 47 orders had gone through. All digital gift cards. All different cards. All shipping to a single address in a country he had never sold to before.&lt;/p&gt;

&lt;p&gt;By the time his payment processor flagged the pattern, the chargebacks were already rolling in. Three weeks later, he had eaten $4,200 in losses, spent hours filing disputes, and nearly lost his merchant account.&lt;/p&gt;

&lt;p&gt;Here is the part that stings: a five-dollar API call on each of those orders would have caught it.&lt;/p&gt;

&lt;p&gt;That is what BIN lookups do. The Bank Identification Number is the first six to eight digits of any payment card. It tells you who issued the card, what country it came from, whether it is a credit or debit card, and whether it is prepaid. None of that stops a determined fraudster forever, but it stops lazy fraud instantly and gives your risk engine real data to work with.&lt;/p&gt;

&lt;p&gt;This tutorial walks you through integrating a BIN API in Python, handling it properly, and turning the response fields into actual fraud rules you can ship.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;A BIN is the first 6-8 digits of a card number. It identifies the issuing bank, country, card type, and brand.&lt;/li&gt;
&lt;li&gt;You call a BIN API with a GET request, pass your API key in headers, and get a JSON object back.&lt;/li&gt;
&lt;li&gt;Key fields for fraud: &lt;code&gt;Brand&lt;/code&gt;, &lt;code&gt;Type&lt;/code&gt; (debit/credit), &lt;code&gt;CountryName&lt;/code&gt;, and whether the card is prepaid.&lt;/li&gt;
&lt;li&gt;Handle &lt;code&gt;429&lt;/code&gt; rate limit responses with exponential backoff. Always set a timeout.&lt;/li&gt;
&lt;li&gt;Four rule examples are included at the end you can drop into a checkout flow today.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Is a BIN and Why Does It Matter for Fraud?
&lt;/h2&gt;

&lt;p&gt;Every Visa, Mastercard, Amex, or Discover card starts with a block of digits that identifies the issuing bank. That block is the BIN, sometimes called the IIN (Issuer Identification Number). Originally it was always six digits. Most modern implementations support eight.&lt;/p&gt;

&lt;p&gt;When a card number hits your checkout, you already have the BIN before the customer even clicks "place order." That gives you a narrow window to run a check that can tell you quite a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Brand&lt;/strong&gt;: Is it Visa, Mastercard, Amex?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt;: Is it credit, debit, or prepaid?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuer&lt;/strong&gt;: Which bank issued it, and from which country?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Country mismatch&lt;/strong&gt;: Is the billing address in one country while the card was issued in another?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these signals alone proves fraud. But combined with your order data, they give you patterns you can act on.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Do You Call a BIN API in Python?
&lt;/h2&gt;

&lt;p&gt;The mechanics are simple. You send a GET request to the lookup endpoint with the BIN as a query parameter, and your credentials in the request headers.&lt;/p&gt;

&lt;p&gt;Here is a minimal working example using the &lt;code&gt;requests&lt;/code&gt; library:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bsl_your_api_key_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;USER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-uuid-here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.binsearchlookup.com/lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lookup_bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-User-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;USER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;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="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lookup_bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;551029&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole happy path. The API returns JSON immediately, typically under 100ms. The response for BIN &lt;code&gt;551029&lt;/code&gt; looks like this:&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;"bin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"551029"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&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;span class="nl"&gt;"BIN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"551029"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MASTERCARD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEBIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STANDARD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BANK OF MONTREAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"IssuerPhone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+18772255266"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"IssuerUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://www.bmo.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isoCode2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isoCode3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CAN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CountryName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CANADA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"similarBins"&lt;/span&gt;&lt;span class="p"&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;span class="nl"&gt;"cached"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"responseTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responseTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;17&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;The &lt;code&gt;data&lt;/code&gt; object is where you will spend most of your time. Notice &lt;code&gt;Brand&lt;/code&gt;, &lt;code&gt;Type&lt;/code&gt;, &lt;code&gt;CountryName&lt;/code&gt;, and &lt;code&gt;isoCode2&lt;/code&gt;. Those four fields cover the majority of fraud rules you will want to write.&lt;/p&gt;

&lt;p&gt;If you want to test the API without wiring up a full integration, &lt;a href="https://www.binsearchlookup.com/" rel="noopener noreferrer"&gt;binsearchlookup.com&lt;/a&gt; offers a free tier with 1,000 requests per month. That is enough to validate your logic and run experiments before committing to a paid plan.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Do You Handle Errors, Timeouts, and Rate Limits?
&lt;/h2&gt;

&lt;p&gt;The happy path is 10 lines of code. The production-ready version is where most tutorials stop being honest with you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeouts
&lt;/h3&gt;

&lt;p&gt;Always pass a &lt;code&gt;timeout&lt;/code&gt; argument to &lt;code&gt;requests.get&lt;/code&gt;. Without it, a slow DNS response or a hiccup on the API side will hang your checkout thread indefinitely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five seconds is generous for an API that normally responds in under 100ms. You may want to tighten it to two seconds if BIN lookup is blocking your order flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP Errors
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;raise_for_status()&lt;/code&gt; converts any 4xx or 5xx into a Python exception, which is fine for development. In production, you want to handle specific codes differently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;requests.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ConnectionError&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lookup_bin_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-User-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;USER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Bad BIN format - your problem, not theirs
&lt;/span&gt;            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid BIN submitted: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API credentials are invalid or missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Rate limited - handle with backoff (see next section)
&lt;/span&gt;            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RateLimitError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rate limit exceeded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;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="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BIN lookup timed out for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ConnectionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Could not reach BIN API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;None&lt;/code&gt; return on timeout is intentional. Whether a failed lookup should block the order or let it through is a business decision, not a technical one. Make that call explicitly rather than letting an exception bubble up unhandled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate Limit Handling With Exponential Backoff
&lt;/h3&gt;

&lt;p&gt;The free tier allows 20 requests per minute. The starter tier allows 60. If you hit the ceiling, you get a &lt;code&gt;429&lt;/code&gt; with error code &lt;code&gt;RATE_LIMIT_EXCEEDED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not retry immediately. Use backoff:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RateLimitError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lookup_bin_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;lookup_bin_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RateLimitError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;  &lt;span class="c1"&gt;# 1s, 2s, 4s
&lt;/span&gt;            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rate limited. Retrying in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Giving up on BIN lookup for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; after &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a high-volume checkout flow, you probably want to queue lookups rather than retry inline. But for small-to-medium traffic, this gets you through the rough spots.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Can You Do With BIN Data in Your Code?
&lt;/h2&gt;

&lt;p&gt;This is the question that matters. The API is not the point. The point is what you build on top of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extracting the Fields You Care About
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_bin_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;card_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;     &lt;span class="c1"&gt;# CREDIT, DEBIT
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;  &lt;span class="c1"&gt;# STANDARD, PREMIUM, etc.
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issuer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Issuer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isoCode2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CountryName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_prepaid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PREPAID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;is_prepaid&lt;/code&gt; flag. Prepaid cards show up as &lt;code&gt;Type: PREPAID&lt;/code&gt; in the response. They are not inherently fraudulent, but in certain product categories (high-value electronics, digital goods, gift cards), prepaid usage rates in chargebacks are disproportionately high.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking Country Mismatch
&lt;/h3&gt;

&lt;p&gt;The most common signal you will want to build is a mismatch between the card-issuing country and the order's billing or shipping country.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_country_mismatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;billing_country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;card_country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;card_country&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;billing_country&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your customer says they are in Germany but the card was issued in Nigeria, that is worth flagging. Not blocking automatically, but flagging.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fraud Rule Playbook: 4 Rules You Can Ship Today
&lt;/h2&gt;

&lt;p&gt;These are not theoretical. They are the patterns that catch the most common carding attacks and reseller abuse. Each one is a function that takes a parsed BIN response plus some order context and returns a risk signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 1: Flag Prepaid Cards on Digital Goods Orders
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flag_prepaid_digital&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;is_digital&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gift_card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;software_license&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subscription&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;is_prepaid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_prepaid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_prepaid&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;is_digital&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prepaid card used for digital goods&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manual_review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rule 2: Block High-Risk Country and Card-Country Mismatch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;HIGH_RISK_COUNTRIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flag_high_risk_origin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;billing_country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;card_country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mismatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card_country&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;billing_country&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;high_risk_origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card_country&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;HIGH_RISK_COUNTRIES&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mismatch&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;high_risk_origin&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Card issued in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;card_country&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, billing address in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;billing_country&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust &lt;code&gt;HIGH_RISK_COUNTRIES&lt;/code&gt; based on your actual chargeback data. This is a starting point, not a permanent list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 3: Elevated Review for First-Time Amex Users
&lt;/h3&gt;

&lt;p&gt;Amex cards carry higher chargeback rights for cardholders, which makes them attractive for friendly fraud. It is worth adding friction for first-time Amex orders above a threshold.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flag_new_amex_high_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;bin_info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_order_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;is_amex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AMERICAN EXPRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;is_new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer_order_count&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;is_high_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_amex&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;is_new&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;is_high_value&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;First-time customer, Amex card, high-value order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;require_verification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rule 4: Velocity Check on Same BIN Across Multiple Orders
&lt;/h3&gt;

&lt;p&gt;If the same BIN appears across five or more orders in a short window, that is a carding pattern. Someone is testing card numbers that share the same issuer block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;order_log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flag_bin_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;window_minutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Filter to recent hits
&lt;/span&gt;    &lt;span class="n"&gt;order_log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;order_log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;order_log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BIN &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; used &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; times in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;window_minutes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; minutes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block_and_alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, back &lt;code&gt;order_log&lt;/code&gt; with Redis rather than an in-memory dict. But the logic transfers directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It All Together in a Checkout Hook
&lt;/h2&gt;

&lt;p&gt;Here is how all four rules look wired together in a single function you could drop into a pre-authorization hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate_card_risk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;card_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;billing_country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;product_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_order_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;bin_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card_number&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Use 8 digits if available
&lt;/span&gt;    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lookup_bin_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Could not reach API - decide your fallback policy here
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BIN lookup unavailable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;bin_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_bin_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bin_info&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown BIN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;checks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;flag_prepaid_digital&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_type&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;flag_high_risk_origin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;billing_country&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;flag_new_amex_high_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_order_count&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;flag_bin_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;  &lt;span class="c1"&gt;# Return first hit
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flagged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Passed all checks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs synchronously and returns quickly. The most expensive part is the HTTP call, which you can cache by BIN. Most BINs do not change. Cache the result for 24 hours and you will cut your API usage by 80% or more on repeat customers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Batch Lookups for Order Review Queues
&lt;/h2&gt;

&lt;p&gt;If you are running a manual review queue or backfilling risk scores on historical orders, use the batch endpoint instead of looping single lookups. It accepts up to 50 BINs per request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;batch_lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.binsearchlookup.com/lookup/batch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-User-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;USER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bins&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin_list&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;  &lt;span class="c1"&gt;# Hard cap at 50
&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in chunks if you have more than 50 BINs to process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunked_batch_lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_list&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;batch_lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What About False Positives?
&lt;/h2&gt;

&lt;p&gt;Every rule in the playbook above will occasionally flag a legitimate customer. A Canadian tourist buying software on a US site. An expat using a prepaid card because they do not have a local bank account yet.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;action&lt;/code&gt; field in each response is intentional. "Block" is for patterns so clear that the false positive rate is low enough to accept. "Manual review" is for signals that need a human eye. "Require verification" is for middle-ground cases where you ask the customer to confirm their identity before proceeding.&lt;/p&gt;

&lt;p&gt;Build your actions around your chargeback rate and your customer tolerance. If you run a $20 SaaS product, a false block costs you a customer. If you run a $2,000 electronics store, a false allow costs you $2,000 and your merchant account standing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;BIN lookups are one of the cheapest and most underused tools in a payment developer's kit. The integration takes under an hour. The &lt;code&gt;requests&lt;/code&gt; library handles everything. The response fields are clean and immediately usable in business logic.&lt;/p&gt;

&lt;p&gt;The four rules in this tutorial are a starting point. Your actual fraud patterns will be specific to your product, your customer base, and your geography. Start with these, watch your flagged order queue for a few weeks, and adjust based on what you actually see.&lt;/p&gt;

&lt;p&gt;The API work is the easy part. The judgment calls about what to block versus review versus allow are where the real work happens. Get the data in front of you first. Everything else follows from there.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Build a BIN Lookup Middleware in Node.js in Under an Hour</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Fri, 27 Mar 2026 21:37:26 +0000</pubDate>
      <link>https://dev.to/rabbitholeinphp/how-to-build-a-bin-lookup-middleware-in-nodejs-in-under-an-hour-j6f</link>
      <guid>https://dev.to/rabbitholeinphp/how-to-build-a-bin-lookup-middleware-in-nodejs-in-under-an-hour-j6f</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A BIN (Bank Identification Number) is the first 6 to 8 digits of any payment card and tells you the issuing bank, card type, country of origin, and whether the card is prepaid or credit.&lt;/li&gt;
&lt;li&gt;You will build a reusable Express middleware function that intercepts incoming card data, calls a BIN lookup API, and attaches structured metadata to &lt;code&gt;req.binInfo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The final middleware lets you block prepaid cards, filter by country, flag high-risk issuers, and add card branding in one clean layer before your payment logic runs.&lt;/li&gt;
&lt;li&gt;The API requires two credentials stored in &lt;code&gt;.env&lt;/code&gt;: your API key and your User ID. Both are sent as request headers on every lookup call.&lt;/li&gt;
&lt;li&gt;BIN values sent to the API must be between 6 and 8 digits. Fewer than 6 cannot identify an issuer. More than 8 starts encoding the cardholder account number, which you must never send to a third-party service.&lt;/li&gt;
&lt;li&gt;Stack: Node.js 18+, Express 4, Axios, dotenv.&lt;/li&gt;
&lt;li&gt;Total build time: under 60 minutes if you follow every step.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why BIN Lookup Middleware Is Worth Your Time
&lt;/h2&gt;

&lt;p&gt;If you process payments, subscriptions, or any kind of card-based transaction, you already know that not all cards are created equal.&lt;/p&gt;

&lt;p&gt;A prepaid card from a high-risk country behaves very differently from a corporate card issued by a bank in a regulated market. Your fraud rules should know the difference before a charge attempt ever reaches Stripe, Braintree, or your processor of choice.&lt;/p&gt;

&lt;p&gt;The cleanest place to handle that logic is middleware. You run the BIN check once, attach the result to the request object, and every subsequent handler in your chain can read from &lt;code&gt;req.binInfo&lt;/code&gt; without making a second API call. Your fraud logic, logging layer, and analytics all pull from the same enriched source.&lt;/p&gt;

&lt;p&gt;This is a pattern used in production payment platforms. It is not complicated once you see it laid out step by step.&lt;/p&gt;

&lt;p&gt;Let us build it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you write a single line of code, make sure your environment is ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node.js version:&lt;/strong&gt; 18 or higher. Run &lt;code&gt;node -v&lt;/code&gt; to check. Node 18 ships with the native &lt;code&gt;fetch&lt;/code&gt; API, but we will use Axios in this tutorial because it gives you cleaner error handling and automatic JSON parsing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Package manager:&lt;/strong&gt; npm or yarn. Either works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Packages you will install:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;express&lt;/code&gt; (version 4.x) for the HTTP server and routing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;axios&lt;/code&gt; for HTTP requests to the BIN API&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotenv&lt;/code&gt; for managing your API credentials as environment variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A BIN lookup API key and User ID:&lt;/strong&gt; This tutorial uses &lt;a href="https://www.binsearchlookup.com/" rel="noopener noreferrer"&gt;binsearchlookup.com&lt;/a&gt; for the lookup calls. Sign up and grab both your API key and your User ID from the account dashboard. You will need both in Step 2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A tool for sending test requests:&lt;/strong&gt; curl, Postman, or the VS Code REST Client extension all work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Set Up a Minimal Express Server
&lt;/h2&gt;

&lt;p&gt;Create a new project folder and initialize it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;bin-middleware-demo
&lt;span class="nb"&gt;cd &lt;/span&gt;bin-middleware-demo
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express axios dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create your entry file and the middleware directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;index.js
&lt;span class="nb"&gt;touch&lt;/span&gt; .env
&lt;span class="nb"&gt;mkdir &lt;/span&gt;middleware
&lt;span class="nb"&gt;touch &lt;/span&gt;middleware/binLookup.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;index.js&lt;/code&gt; and write the skeleton server:&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="c1"&gt;// index.js&lt;/span&gt;
&lt;span class="c1"&gt;// Load environment variables from .env before anything else&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&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;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Parse incoming JSON bodies&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;binLookupMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./middleware/binLookup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Apply the BIN middleware only to the validate-card route&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/validate-card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;binLookupMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="c1"&gt;// At this point req.binInfo is already populated by the middleware&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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="na"&gt;success&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="na"&gt;binInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binInfo&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;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&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;Run &lt;code&gt;node index.js&lt;/code&gt; and confirm the server starts. You should see the log line in your terminal. Move on to credentials next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Store Both Credentials Safely
&lt;/h2&gt;

&lt;p&gt;The BIN lookup API requires two headers on every request: your API key and your User ID. Both live in your &lt;code&gt;.env&lt;/code&gt; file so they never appear hardcoded in source code.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;.env&lt;/code&gt; and add your credentials using the exact variable names shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env

# Your API key from your binsearchlookup.com dashboard
X-API-Key=your_api_key_here

# Your User ID from your binsearchlookup.com dashboard
X-User-ID=your_user_id_here

PORT=3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;your_api_key_here&lt;/code&gt; and &lt;code&gt;your_user_id_here&lt;/code&gt; with the actual values from your dashboard. Never paste real credentials into tutorials, blog posts, or any file that goes into version control.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;.env&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt; right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one habit prevents credential leaks. Your code reads the values at runtime using &lt;code&gt;process.env["X-API-Key"]&lt;/code&gt; and &lt;code&gt;process.env["X-User-ID"]&lt;/code&gt;. No hardcoded strings, ever.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why two credentials?&lt;/strong&gt; The API key authenticates your application. The User ID identifies your specific account so the provider can apply per-account rate limits, usage tracking, and billing. Both are required on every request. A call missing either one will be rejected.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 3: Extract the BIN From the Request
&lt;/h2&gt;

&lt;p&gt;The BIN is the first 6 to 8 digits of a card number. In real integrations you often receive the full card number on a server-to-server call, or a tokenized version that still carries the BIN prefix.&lt;/p&gt;

&lt;p&gt;For this tutorial the client sends a JSON body like this:&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;"cardNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5184369287452458"&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;Your middleware needs to extract the first 8 digits, then validate that the result falls within the required 6-to-8 digit range. Fewer than 6 digits cannot identify a unique issuer. More than 8 digits starts encoding the individual account number, which is sensitive cardholder data you must never send to any third-party API.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;middleware/binLookup.js&lt;/code&gt; and write the extraction logic:&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="c1"&gt;// middleware/binLookup.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// BIN must be between 6 and 8 digits -- no more, no less.&lt;/span&gt;
&lt;span class="c1"&gt;// Fewer than 6 cannot identify a unique issuer.&lt;/span&gt;
&lt;span class="c1"&gt;// More than 8 encodes the account number, which is sensitive cardholder data.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractBin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&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;cardNumber&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Remove any spaces or dashes the client might have included&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\D&lt;/span&gt;&lt;span class="sr"&gt;/g&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="c1"&gt;// Take the first 8 digits at most&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Reject if we cannot produce at least 6 digits&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;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;bin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;binLookupMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cardNumber&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractBin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Strict range check: the BIN must be 6, 7, or 8 digits exactly&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;bin&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BIN must be between 6 and 8 digits. Please provide a valid card number.&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="c1"&gt;// Attach the raw BIN to req for reference downstream&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// The API call happens in the next step&lt;/span&gt;
  &lt;span class="nf"&gt;next&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;Notice that the middleware calls &lt;code&gt;next()&lt;/code&gt; at the end. That is the Express convention. When you are done with your work, you hand control to the next handler in the chain. If you need to stop the request early, like the 400 response above, you return the response directly without calling &lt;code&gt;next()&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Call the BIN Lookup API
&lt;/h2&gt;

&lt;p&gt;Now replace the placeholder &lt;code&gt;next()&lt;/code&gt; with the actual API request. You will call the BIN lookup service, receive a structured response, and attach it to &lt;code&gt;req.binInfo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The API accepts your BIN as a query parameter and expects both credential headers on every call. Here is what a real successful response looks like for BIN &lt;code&gt;518436&lt;/code&gt;:&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;"bin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"518436"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&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;span class="nl"&gt;"BIN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"518436"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"validity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CREDIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MASTERCARD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prepaid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TITANIUM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ABU DHABI ISLAMIC BANK P.J.S.C."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"IssuerPhone"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"IssuerUrl"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pan_length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isoCode2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isoCode3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ARE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"United Arab Emirates"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"responseTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&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;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responseTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&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;Notice the structure. The useful fields live inside &lt;code&gt;data&lt;/code&gt;: card type, brand, prepaid flag, issuer name, and country codes. You will reference &lt;code&gt;req.binInfo.data&lt;/code&gt; in your business rules.&lt;/p&gt;

&lt;p&gt;Now write the full middleware. Replace the entire contents of &lt;code&gt;middleware/binLookup.js&lt;/code&gt;:&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="c1"&gt;// middleware/binLookup.js -- full version with API call&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// BIN must be exactly 6 to 8 digits.&lt;/span&gt;
&lt;span class="c1"&gt;// Anything shorter cannot identify an issuer.&lt;/span&gt;
&lt;span class="c1"&gt;// Anything longer encodes the account number -- never send that to a third party.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractBin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&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;cardNumber&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\D&lt;/span&gt;&lt;span class="sr"&gt;/g&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&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;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;bin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;binLookupMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cardNumber&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractBin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&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;bin&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BIN must be between 6 and 8 digits. Please provide a valid card number.&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bin&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="nx"&gt;axios&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;https://api.binsearchlookup.com/lookup&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;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Pass the extracted BIN as a query parameter&lt;/span&gt;
          &lt;span class="c1"&gt;// The API accepts 6, 7, or 8 digits&lt;/span&gt;
          &lt;span class="na"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Both headers are required on every request&lt;/span&gt;
          &lt;span class="c1"&gt;// Both values are read from environment variables -- never hardcode them&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-API-Key&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;X-User-ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-User-ID&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="c1"&gt;// Fail fast: if the BIN API takes longer than 3 seconds, soft-fail&lt;/span&gt;
        &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&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;// Attach the full enriched BIN response to the request object.&lt;/span&gt;
    &lt;span class="c1"&gt;// Every downstream handler can now read req.binInfo without a second API call.&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binInfo&lt;/span&gt; &lt;span class="o"&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;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;next&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;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Log the failure internally -- never expose credentials or internals to the client&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BIN lookup failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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="c1"&gt;// Soft-fail: set binInfo to null and continue.&lt;/span&gt;
    &lt;span class="c1"&gt;// A BIN API outage should not take your entire checkout flow down.&lt;/span&gt;
    &lt;span class="c1"&gt;// Your route handler decides what to do when binInfo is null.&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binInfo&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="nf"&gt;next&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;Your middleware now handles both credentials from &lt;code&gt;.env&lt;/code&gt;, enforces the BIN digit range, and attaches everything downstream handlers need.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Use the Metadata for Real Business Rules
&lt;/h2&gt;

&lt;p&gt;The middleware has done its job. &lt;code&gt;req.binInfo&lt;/code&gt; is populated with the full API response. The actual card data lives inside &lt;code&gt;req.binInfo.data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;index.js&lt;/code&gt; and replace the skeleton route handler with real policy logic:&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="c1"&gt;// index.js -- full version with business rules&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&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;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;binLookupMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./middleware/binLookup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Cards issued outside this list will be blocked&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALLOWED_COUNTRIES&lt;/span&gt; &lt;span class="o"&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;US&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;CA&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;GB&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;AU&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;DE&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;FR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/validate-card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;binLookupMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;binInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binInfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// BIN lookup failed (API outage or unknown BIN) -- block and ask to retry&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;binInfo&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;binInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unable to verify card origin. Please try again.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;binInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Rule 1: Block prepaid cards.&lt;/span&gt;
  &lt;span class="c1"&gt;// Prepaid cards are high-risk for chargebacks and subscription fraud.&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;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prepaid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Prepaid cards are not accepted for this service.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PREPAID_CARD_BLOCKED&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="c1"&gt;// Rule 2: Block cards issued outside your allowed countries.&lt;/span&gt;
  &lt;span class="c1"&gt;// cardData.isoCode2 is the ISO 3166-1 two-letter country code.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issuerCountry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isoCode2&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;issuerCountry&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_COUNTRIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issuerCountry&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cards from this region are not supported.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COUNTRY_NOT_ALLOWED&lt;/span&gt;&lt;span class="dl"&gt;"&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;issuerCountry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;countryName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&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="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// All checks passed. Return enriched card metadata to the caller.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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="na"&gt;success&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="na"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cardBrand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cardType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cardCategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Issuer&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;issuerCountry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;countryName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&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="na"&gt;prepaid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prepaid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;panLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pan_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Card validated successfully.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&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;Each rule is a single conditional block. You can add, remove, or reorder rules without touching the middleware at all. The middleware fetches and attaches. The route handler enforces policy. Two different responsibilities, two different places.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Test Every Scenario With Curl
&lt;/h2&gt;

&lt;p&gt;Start your server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test 1: A valid Mastercard Titanium credit card&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/validate-card &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"cardNumber": "5184369287452458"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected response:&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;"bin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"518436"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&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;span class="nl"&gt;"BIN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"518436"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"validity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CREDIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MASTERCARD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prepaid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TITANIUM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ABU DHABI ISLAMIC BANK P.J.S.C."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"IssuerPhone"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"IssuerUrl"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pan_length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isoCode2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isoCode3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ARE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"United Arab Emirates"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"responseTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&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;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responseTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&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;Note that &lt;code&gt;AE&lt;/code&gt; is not in the &lt;code&gt;ALLOWED_COUNTRIES&lt;/code&gt; list, so this specific BIN will trigger the country block rule. That is intentional. It lets you confirm that Rule 2 works correctly. To test a passing card, use a BIN issued in the US, CA, GB, AU, DE, or FR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 2: A prepaid card&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/validate-card &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"cardNumber": "4023601234567890"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: &lt;code&gt;403 Forbidden&lt;/code&gt; with &lt;code&gt;PREPAID_CARD_BLOCKED&lt;/code&gt; code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 3: Missing card number&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/validate-card &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: &lt;code&gt;400 Bad Request&lt;/code&gt; with a BIN digit range error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 4: Card number too short to produce a valid BIN&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/validate-card &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"cardNumber": "411"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: &lt;code&gt;400 Bad Request&lt;/code&gt;. The extracted BIN is only 3 digits, which is below the required minimum of 6.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 5: Card issued in a blocked country&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use a BIN from an issuer outside your &lt;code&gt;ALLOWED_COUNTRIES&lt;/code&gt; list.&lt;br&gt;
Expected: &lt;code&gt;403 Forbidden&lt;/code&gt; with &lt;code&gt;COUNTRY_NOT_ALLOWED&lt;/code&gt;, the alpha2 code, and the full country name in the response body.&lt;/p&gt;

&lt;p&gt;Run all five tests before moving on. If any response is unexpected, re-read the middleware and double-check your &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 7: Add a Simple In-Memory Cache (Optional but Recommended)
&lt;/h2&gt;

&lt;p&gt;You do not need to call the BIN API on every single request. A BIN is permanently tied to an issuer. It does not change. If the same BIN appears twice in a session or across any requests, serve it from cache.&lt;/p&gt;

&lt;p&gt;Here is a lightweight in-memory cache using a plain JavaScript &lt;code&gt;Map&lt;/code&gt;:&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="c1"&gt;// middleware/binLookup.js -- final version with caching&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Simple in-memory cache: BIN string -&amp;gt; API response object.&lt;/span&gt;
&lt;span class="c1"&gt;// In production, replace this with Redis and set a TTL of 24 hours.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;binCache&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractBin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&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;cardNumber&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\D&lt;/span&gt;&lt;span class="sr"&gt;/g&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&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;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;bin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;binLookupMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cardNumber&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractBin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&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;bin&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BIN must be between 6 and 8 digits. Please provide a valid card number.&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Serve from cache if available -- avoids a network call entirely&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;binCache&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;bin&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;binCache&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="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binCacheHit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Useful for metrics and logging&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="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="nx"&gt;axios&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;https://api.binsearchlookup.com/lookup&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;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;headers&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;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-API-Key&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;X-User-ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-User-ID&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;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&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;// Store result in cache before attaching to req&lt;/span&gt;
    &lt;span class="nx"&gt;binCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bin&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;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binInfo&lt;/span&gt; &lt;span class="o"&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;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binCacheHit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;next&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;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BIN lookup failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binInfo&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binCacheHit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;next&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;For a production system, replace the &lt;code&gt;Map&lt;/code&gt; with a Redis client and set a TTL of 24 hours. BIN data is stable, and Redis handles memory management, persistence, and expiry automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Structure Your Project for the Long Term
&lt;/h2&gt;

&lt;p&gt;Here is the final folder structure you should have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin-middleware-demo/
  middleware/
    binLookup.js       # Fetches BIN data and attaches it to req.binInfo
  index.js             # Express server, route definitions, and business rules
  .env                 # API credentials and config (never commit this)
  .gitignore
  package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As your project grows, split the business rules into a separate &lt;code&gt;validators/cardPolicy.js&lt;/code&gt; file and import it into the route handler. The middleware stays thin and focused on fetching. The policy layer stays focused on rules. Clean separation means easier unit testing and easier onboarding for every engineer who joins the project after you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Code Reference
&lt;/h2&gt;

&lt;p&gt;Here is every file in its final state so you can copy, paste, and run without scrolling back through the steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.env&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env
# Get both values from your binsearchlookup.com account dashboard.
# Never commit this file. It is already in .gitignore.

X-API-Key=your_api_key_here
X-User-ID=your_user_id_here

PORT=3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;middleware/binLookup.js&lt;/code&gt;
&lt;/h3&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;axios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// In-memory cache to avoid redundant API calls for the same BIN.&lt;/span&gt;
&lt;span class="c1"&gt;// Replace with Redis in production and set a 24-hour TTL.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;binCache&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// BIN must be between 6 and 8 digits.&lt;/span&gt;
&lt;span class="c1"&gt;// 6 digits is the minimum to identify a unique issuer.&lt;/span&gt;
&lt;span class="c1"&gt;// 8 digits is the maximum before you start encoding the account number.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractBin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&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;cardNumber&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\D&lt;/span&gt;&lt;span class="sr"&gt;/g&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&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;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;bin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;binLookupMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cardNumber&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractBin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardNumber&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;bin&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BIN must be between 6 and 8 digits. Please provide a valid card number.&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Return cached result if available&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;binCache&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;bin&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;binCache&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="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binCacheHit&lt;/span&gt; &lt;span class="o"&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="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="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="nx"&gt;axios&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;https://api.binsearchlookup.com/lookup&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;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// X-API-Key and X-User-ID are both required by the API.&lt;/span&gt;
          &lt;span class="c1"&gt;// Both are read from .env -- never hardcode credentials.&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-API-Key&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;X-User-ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-User-ID&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;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&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;binCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bin&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;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binInfo&lt;/span&gt; &lt;span class="o"&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;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binCacheHit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;next&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;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Soft-fail: log internally, do not expose API details to the client&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BIN lookup failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binInfo&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binCacheHit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;next&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;h3&gt;
  
  
  &lt;code&gt;index.js&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&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;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;binLookupMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./middleware/binLookup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Cards issued outside this list will be blocked.&lt;/span&gt;
&lt;span class="c1"&gt;// Extend this array to match your business requirements.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALLOWED_COUNTRIES&lt;/span&gt; &lt;span class="o"&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;US&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;CA&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;GB&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;AU&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;DE&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;FR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/validate-card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;binLookupMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;binInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binInfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// BIN lookup failed (API outage or unknown BIN) -- block and ask to retry&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;binInfo&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;binInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unable to verify card origin. Please try again.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;binInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Rule 1: Block prepaid cards&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;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prepaid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Prepaid cards are not accepted for this service.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PREPAID_CARD_BLOCKED&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="c1"&gt;// Rule 2: Block cards from unsupported issuer countries&lt;/span&gt;
  &lt;span class="c1"&gt;// cardData.isoCode2 holds the ISO 3166-1 alpha-2 country code&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issuerCountry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isoCode2&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;issuerCountry&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_COUNTRIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issuerCountry&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cards from this region are not supported.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COUNTRY_NOT_ALLOWED&lt;/span&gt;&lt;span class="dl"&gt;"&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;issuerCountry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;countryName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&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="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// All checks passed&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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="na"&gt;success&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="na"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cardBrand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cardType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cardCategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Issuer&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;issuerCountry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;countryName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&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="na"&gt;prepaid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prepaid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;panLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pan_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cacheHit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;binCacheHit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Card validated successfully.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&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;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;You built a production-quality BIN lookup middleware in eight steps. Here is what you now have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A reusable &lt;code&gt;binLookupMiddleware&lt;/code&gt; function that extracts the BIN, enforces the strict 6-to-8 digit rule, calls the lookup API with both required credentials, and attaches the full structured response to &lt;code&gt;req.binInfo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Both &lt;code&gt;X-API-Key&lt;/code&gt; and &lt;code&gt;X-User-ID&lt;/code&gt; stored safely in &lt;code&gt;.env&lt;/code&gt; and passed as headers on every API request. Neither credential ever appears in source code.&lt;/li&gt;
&lt;li&gt;Business rules in the route handler that read from &lt;code&gt;req.binInfo.data&lt;/code&gt; to block prepaid cards and cards from unsupported countries.&lt;/li&gt;
&lt;li&gt;Graceful soft-fail handling so an API outage does not break your checkout flow.&lt;/li&gt;
&lt;li&gt;An in-memory cache to eliminate redundant network calls for repeated BINs.&lt;/li&gt;
&lt;li&gt;Clean separation between the data-fetching middleware and the business rule layer in the route handler.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern you learned here applies beyond BIN lookups. Any time you need to enrich an incoming request with external data before your main handler runs, middleware is the right tool. IP geolocation, device fingerprinting, user reputation scoring: all of them follow exactly this shape.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is a BIN number and why does it matter for payments?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;BIN stands for Bank Identification Number. It is the first 6 to 8 digits of a payment card number and it identifies the issuing bank, the card network (Visa, Mastercard, Amex), the card type (credit, debit, prepaid), and the country where the card was issued. Payment platforms use BIN data to run fraud checks, apply geographic restrictions, and display the correct card brand logo at checkout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does the BIN value sent to the API have to be between 6 and 8 digits?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The BIN lookup database is indexed on 6-digit and 8-digit prefixes. Fewer than 6 digits does not uniquely identify an issuer. More than 8 digits begins encoding the individual account number, which is sensitive cardholder data that you must never send to any third-party service. The &lt;code&gt;extractBin&lt;/code&gt; function enforces this range before the network call is ever made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are X-API-Key and X-User-ID and why are both required?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;X-API-Key&lt;/code&gt; authenticates your application to the BIN lookup API. &lt;code&gt;X-User-ID&lt;/code&gt; identifies the specific account making the request, which allows the API provider to apply per-account rate limits, usage tracking, and billing. Both are assigned when you create an account at binsearchlookup.com. Storing them in &lt;code&gt;.env&lt;/code&gt; with the exact names &lt;code&gt;X-API-Key&lt;/code&gt; and &lt;code&gt;X-User-ID&lt;/code&gt; and reading them with &lt;code&gt;process.env["X-API-Key"]&lt;/code&gt; and &lt;code&gt;process.env["X-User-ID"]&lt;/code&gt; keeps them out of your source code and out of version control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why use bracket notation like &lt;code&gt;process.env["X-API-Key"]&lt;/code&gt; instead of &lt;code&gt;process.env.X_API_KEY&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dot notation in JavaScript only works for property names that are valid identifiers. The hyphen character in &lt;code&gt;X-API-Key&lt;/code&gt; and &lt;code&gt;X-User-ID&lt;/code&gt; is not valid in a dot-notation identifier, so you must use bracket notation to access those keys from &lt;code&gt;process.env&lt;/code&gt;. Alternatively you can rename the variables in &lt;code&gt;.env&lt;/code&gt; to use underscores (for example &lt;code&gt;X_API_KEY&lt;/code&gt;) and access them with &lt;code&gt;process.env.X_API_KEY&lt;/code&gt;, but keeping the original header names as the variable names makes it immediately obvious which &lt;code&gt;.env&lt;/code&gt; value maps to which HTTP header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it safe to log the full card number on the server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. You should never log a full card number. Extract only the BIN immediately and discard or tokenize the rest. The BIN alone is not sensitive because it does not identify the cardholder or authorize a transaction. Log the BIN, not the card number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the BIN API is down?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The middleware uses a soft-fail pattern. If the API call throws an error or times out, &lt;code&gt;req.binInfo&lt;/code&gt; is set to &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;next()&lt;/code&gt; is called. Your route handler checks for &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;!binInfo.success&lt;/code&gt; and decides the fallback policy. The example in this tutorial returns a 422 and asks the user to try again. You could also allow the transaction through with a reduced confidence score. The decision belongs in the route handler, not in the middleware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use this with Stripe or another payment processor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. This middleware runs before you ever call Stripe. You enrich the request object first, apply your rules, and only then initiate the Stripe payment intent if everything passes. The BIN check adds a validation layer that sits entirely on your own server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use 6-digit or 8-digit BINs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use 8-digit BINs when the card number is long enough to produce them. The payments industry has been migrating toward 8-digit BINs since 2022, and modern BIN lookup APIs return more accurate results with 8 digits. The &lt;code&gt;extractBin&lt;/code&gt; function in this tutorial uses &lt;code&gt;substring(0, 8)&lt;/code&gt; to grab as many digits as possible up to the 8-digit maximum, then validates that at least 6 were available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I extend the middleware to handle debit cards differently?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Read &lt;code&gt;req.binInfo.data.Type&lt;/code&gt; in your route handler. It will be &lt;code&gt;"CREDIT"&lt;/code&gt;, &lt;code&gt;"DEBIT"&lt;/code&gt;, or &lt;code&gt;"PREPAID"&lt;/code&gt;. Write conditional logic for each type without touching the middleware at all. That is the benefit of keeping data fetching and policy enforcement in separate layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the right timeout for a BIN lookup API call?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A 2 to 3 second timeout is reasonable. BIN lookups are fast because the data is static and well-indexed. If your upstream API consistently takes longer than 1 second, check whether you are hitting a rate limit or whether a geographically closer endpoint is available.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this tutorial helped you, share what rules you are enforcing in your own BIN middleware in the comments. The more specific your use case, the more others in the community can learn from it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>api</category>
      <category>backend</category>
    </item>
    <item>
      <title>How to Validate Cards Properly with BIN Intelligence</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Tue, 24 Mar 2026 01:11:25 +0000</pubDate>
      <link>https://dev.to/rabbitholeinphp/how-to-validate-cards-properly-with-bin-intelligence-3ien</link>
      <guid>https://dev.to/rabbitholeinphp/how-to-validate-cards-properly-with-bin-intelligence-3ien</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Regex and Luhn checks confirm a card &lt;em&gt;looks&lt;/em&gt; valid. They do not confirm it &lt;em&gt;is&lt;/em&gt; valid or accepted by your processor.&lt;/li&gt;
&lt;li&gt;The Bank Identification Number (BIN), also called the Issuer Identification Number (IIN), encodes the card brand, funding type, issuer country, and category before you ever send a single auth request.&lt;/li&gt;
&lt;li&gt;Querying a BIN API at checkout lets you block prepaid cards, flag cross-border risk, and route to the right processor before you spend an auth fee.&lt;/li&gt;
&lt;li&gt;A production-grade validation flow layers regex, Luhn, BIN lookup, and downstream processor response handling in sequence, not in isolation.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Regex Validation Is Not Enough?
&lt;/h2&gt;

&lt;p&gt;Most teams start with the same approach. A card number arrives at checkout, you run a pattern match, and if it passes you fire off the authorization. It feels reasonable. It is not sufficient.&lt;/p&gt;

&lt;p&gt;Here is what a standard naive validation stack looks like:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;luhn_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&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;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;card_number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;odd_digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;even_digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;odd_digits&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;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;even_digits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;divmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;naive_validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;card_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card_number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;patterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visa&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^4[0-9]{12}(?:[0-9]{3})?$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mastercard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^5[1-5][0-9]{14}$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^3[47][0-9]{13}$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discover&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^6(?:011|5[0-9]{2})[0-9]{12}$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card_number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format_valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;luhn_valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;luhn_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;brand&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 code is not wrong. It is incomplete. Here is what it cannot tell you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The card is prepaid.&lt;/strong&gt; Your platform prohibits prepaid cards for subscription billing, but this regex has no way to detect that. The card passes validation, the customer subscribes, the first renewal fails, and you eat the chargeback cost plus processor fee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The card is from a restricted country.&lt;/strong&gt; Your terms of service block cards issued in certain jurisdictions. The number format is identical to a domestic card. Regex does not encode geography.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The card brand has changed its BIN range.&lt;/strong&gt; Mastercard expanded into 2-series BINs starting with 222100. Visa launched 2-series ranges in some markets. Your regex was written in 2019. The pattern no longer matches a valid card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The card is a commercial purchasing card.&lt;/strong&gt; It carries Level 2 and Level 3 interchange eligibility. You are billing it at Level 1 and leaving hundreds of basis points on the table every month.&lt;/p&gt;

&lt;p&gt;Luhn adds a checksum layer that catches transcription errors and random number generators. It does not catch any of the above. A valid Luhn number on a canceled card is still a valid Luhn number.&lt;/p&gt;

&lt;p&gt;The bottom line: naive validation tells you the number is plausible. BIN intelligence tells you what the card actually is.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Extra Signals Does BIN Give You?
&lt;/h2&gt;

&lt;p&gt;The BIN is the first six to eight digits of a card number. Every issuing bank registers its BIN ranges with the card networks. A BIN lookup decodes those digits against a continuously updated registry and returns structured metadata about the card before authorization.&lt;/p&gt;

&lt;p&gt;Here are the fields that matter in production:&lt;/p&gt;

&lt;h3&gt;
  
  
  Card Type (Funding Source)
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;type&lt;/code&gt; field returns one of: &lt;code&gt;credit&lt;/code&gt;, &lt;code&gt;debit&lt;/code&gt;, or &lt;code&gt;prepaid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This distinction drives real business logic. Prepaid cards cannot be charged again after the initial balance is exhausted. Some regulatory frameworks treat prepaid differently from credit for know-your-customer purposes. Marketplaces that pay out to cards need to know whether the receiving card supports push-to-debit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Card Brand
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;brand&lt;/code&gt; field returns the network: Visa, Mastercard, American Express, Discover, UnionPay, Elo, Maestro, and others. This is more reliable than a regex pattern match because the registry is updated by the networks themselves when BIN ranges change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issuer Country
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;country&lt;/code&gt; field returns the ISO 3166-1 alpha-2 country code of the issuing bank, not the cardholder's location.&lt;/p&gt;

&lt;p&gt;This is material for fraud scoring. A card issued in one country used from an IP address in another, purchasing a digital product that will be delivered to a third country, is a different risk profile than a domestic transaction. You cannot derive the issuer country from the card number format alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prepaid Flag
&lt;/h3&gt;

&lt;p&gt;Some BIN responses return a discrete &lt;code&gt;prepaid&lt;/code&gt; boolean in addition to the type field. Use it. Prepaid detection at checkout, before authorization, prevents a class of dispute where a customer loads a prepaid card, completes a subscription trial, and the renewal fails because the card has no remaining balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Card Category
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;category&lt;/code&gt; field encodes the product type: consumer, commercial, corporate, purchasing, fleet, or signature. Commercial cards are eligible for lower interchange rates when you submit enhanced line-item data. Consumer signature cards often carry higher interchange that you want to route to the right acquirer. Knowing the category at checkout means you can branch your processing logic before the auth request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issuer Name and Phone
&lt;/h3&gt;

&lt;p&gt;Some BIN databases return the issuing bank name and contact number. This is primarily useful for customer service: when a card declines, your support team can advise the customer to call their issuer without looking up which bank issued the card.&lt;/p&gt;

&lt;p&gt;A practical BIN response looks like this. The example below comes from the BinSearchLookup API response format, which is representative of what a well-structured BIN service returns:&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;"bin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"551029"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&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;span class="nl"&gt;"BIN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"551029"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MASTERCARD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEBIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STANDARD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BANK OF MONTREAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"IssuerPhone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+18772255266"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"IssuerUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://www.bmo.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isoCode2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isoCode3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CAN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CountryName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CANADA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"similarBins"&lt;/span&gt;&lt;span class="p"&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;span class="nl"&gt;"cached"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"responseTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responseTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;17&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;Every field in that response can gate a business decision. &lt;code&gt;Type&lt;/code&gt; tells you it is a debit card. &lt;code&gt;Category&lt;/code&gt; tells you it is a standard consumer product. &lt;code&gt;isoCode2&lt;/code&gt; tells you the issuer is Canadian. &lt;code&gt;Issuer&lt;/code&gt; and &lt;code&gt;IssuerPhone&lt;/code&gt; give your support team what they need if the card declines. None of it is derivable from the card number format or the Luhn checksum.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Do You Add BIN Intelligence to an Existing Checkout?
&lt;/h2&gt;

&lt;p&gt;The integration point is always after the customer enters the card number and before the authorization request. In most checkout flows, that means triggering on the &lt;code&gt;blur&lt;/code&gt; event of the card number field client-side, or in the server-side validation layer before you call your payment processor SDK.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where to Call the BIN API
&lt;/h3&gt;

&lt;p&gt;Here is a language-agnostic flow showing the correct sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CHECKOUT VALIDATION FLOW
========================

1. RECEIVE card input
   |
   v
2. FORMAT CHECK (regex + Luhn)
   - Fail fast on obviously invalid numbers
   - Return field-level error to UI immediately
   |
   v
3. EXTRACT BIN (first 6-8 digits)
   |
   v
4. QUERY BIN API
   - Pass: BIN digits
   - Receive: type, brand, country, prepaid, category
   |
   +---&amp;gt; APPLY BUSINESS RULES
         - Is type "prepaid" and prepaid blocked? REJECT
         - Is issuer country in restricted list? FLAG or REJECT
         - Is brand supported by your processor? REJECT
         - Is category "commercial"? ROUTE to Level 2 processor
         - Is brand "AMEX" and you have no Amex agreement? REJECT
   |
   v
5. PROCEED TO AUTHORIZATION
   - Pass BIN metadata to your fraud scoring layer
   - Pass category metadata to your processor for interchange optimization
   |
   v
6. HANDLE PROCESSOR RESPONSE
   - Log decline codes alongside BIN metadata
   - Surface issuer phone number to customer support if needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A Server-Side Implementation Pattern
&lt;/h3&gt;

&lt;p&gt;The example below uses the &lt;a href="https://www.binsearchlookup.com/" rel="noopener noreferrer"&gt;BinSearchLookup API&lt;/a&gt; as one concrete option. Authentication requires two headers: &lt;code&gt;X-API-Key&lt;/code&gt; and &lt;code&gt;X-User-ID&lt;/code&gt;, both available from your account dashboard. The endpoint is a standard GET request with the BIN passed as a query parameter.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c1"&gt;# BinSearchLookup API configuration
# Docs: https://www.binsearchlookup.com/development/docs/
&lt;/span&gt;&lt;span class="n"&gt;BSL_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.binsearchlookup.com/lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BSL_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bsl_your_64_character_key_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# X-API-Key header
&lt;/span&gt;&lt;span class="n"&gt;BSL_USER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-uuid-here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;                    &lt;span class="c1"&gt;# X-User-ID header
&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_card_with_bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;business_rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 1: Basic format and Luhn check
&lt;/span&gt;    &lt;span class="n"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card_number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;luhn_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LUHN_FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 2: Extract BIN (8 digits preferred; BinSearchLookup supports 6-8)
&lt;/span&gt;    &lt;span class="n"&gt;bin_digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 3: BIN lookup via BinSearchLookup
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&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="n"&gt;BSL_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin_digits&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BSL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-User-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BSL_USER_ID&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="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Fail open on BIN API timeout -- do not block the customer
&lt;/span&gt;        &lt;span class="nf"&gt;log_bin_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_digits&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bin_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BIN_TIMEOUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPStatusError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# 429 = rate limit exceeded; 401 = bad credentials; 404 = BIN not found
&lt;/span&gt;        &lt;span class="nf"&gt;log_bin_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_digits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bin_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BIN_HTTP_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# BinSearchLookup wraps data under a "data" key
&lt;/span&gt;    &lt;span class="c1"&gt;# Response fields use PascalCase: Brand, Type, Category, Issuer, isoCode2
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bin_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BIN_LOOKUP_FAILED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;bin_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 4: Apply business rules against the real response fields
&lt;/span&gt;    &lt;span class="n"&gt;card_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# "CREDIT", "DEBIT"
&lt;/span&gt;    &lt;span class="n"&gt;card_brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;# "VISA", "MASTERCARD", etc.
&lt;/span&gt;    &lt;span class="n"&gt;card_category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# "STANDARD", "PREMIUM", etc.
&lt;/span&gt;    &lt;span class="n"&gt;issuer_country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isoCode2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;# ISO 3166-1 alpha-2, e.g. "US"
&lt;/span&gt;    &lt;span class="n"&gt;issuer_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Issuer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;issuer_phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IssuerPhone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# Useful for customer support
&lt;/span&gt;
    &lt;span class="c1"&gt;# Block prepaid: BinSearchLookup surfaces prepaid via Type field
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;business_rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block_prepaid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;card_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PREPAID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PREPAID_NOT_ACCEPTED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Block restricted issuer countries
&lt;/span&gt;    &lt;span class="n"&gt;restricted_countries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;business_rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restricted_countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;issuer_country&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;restricted_countries&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ISSUER_COUNTRY_RESTRICTED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Block unsupported brands
&lt;/span&gt;    &lt;span class="n"&gt;supported_brands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;business_rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supported_brands&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;supported_brands&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;card_brand&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&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;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;supported_brands&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BRAND_NOT_SUPPORTED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bin_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;card_brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;card_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;card_category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issuer_country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;issuer_country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issuer_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;issuer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issuer_phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;issuer_phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# Store this; useful for decline support flows
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_time_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;responseTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to note. The BinSearchLookup response wraps card data inside a &lt;code&gt;data&lt;/code&gt; object and uses PascalCase field names (&lt;code&gt;Brand&lt;/code&gt;, &lt;code&gt;Type&lt;/code&gt;, &lt;code&gt;Category&lt;/code&gt;, &lt;code&gt;Issuer&lt;/code&gt;, &lt;code&gt;isoCode2&lt;/code&gt;), so map them explicitly rather than assuming snake_case. The &lt;code&gt;cached&lt;/code&gt; field tells you whether the result was served from the provider's internal cache, which is useful for debugging unexpected stale data. Store &lt;code&gt;IssuerPhone&lt;/code&gt; alongside the transaction record: when a card declines, your support team can surface the issuer number to the customer without a manual lookup. The timeout is set at 1.5 seconds with fail-open behavior. A BIN API outage should never take down your checkout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client-Side Triggering
&lt;/h3&gt;

&lt;p&gt;Triggering the BIN lookup on the client side, before form submission, improves user experience significantly. You can show the correct card brand logo as the customer types, block disallowed card types before they reach your server, and avoid a round-trip auth request on a card your platform will not accept.&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="c1"&gt;// BinSearchLookup credentials (keep the API key server-side in production;&lt;/span&gt;
&lt;span class="c1"&gt;// this pattern is shown for clarity -- proxy the call through your backend)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BSL_API_KEY&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bsl_your_64_character_key_here&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;BSL_USER_ID&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-uuid-here&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchBinData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;binDigits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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="s2"&gt;`https://api.binsearchlookup.com/lookup?bin=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;binDigits&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="na"&gt;headers&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;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BSL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-User-ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BSL_USER_ID&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="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;res&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="kc"&gt;null&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;payload&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;res&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;// BinSearchLookup wraps data under payload.data with PascalCase keys&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Fail silently on the client; server validates independently&lt;/span&gt;
  &lt;span class="p"&gt;}&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;binLookupTimer&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="c1"&gt;// Trigger BIN lookup after 8 digits are entered&lt;/span&gt;
&lt;span class="nx"&gt;cardInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&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;e&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="nx"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\D&lt;/span&gt;&lt;span class="sr"&gt;/g&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&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;binDigits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Debounce to avoid hammering the API on each keystroke&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;binLookupTimer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;binLookupTimer&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="k"&gt;async &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="nx"&gt;data&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;fetchBinData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;binDigits&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// BinSearchLookup returns PascalCase: Brand, Type, Category&lt;/span&gt;
        &lt;span class="nf"&gt;updateCardBrandDisplay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Brand&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// e.g. "VISA", "MASTERCARD"&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PREPAID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;prepaidBlocked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;showFieldError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;We do not accept prepaid cards.&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="c1"&gt;// Optionally surface the issuer country for fraud pre-screening&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;restrictedCountries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isoCode2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;showFieldError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cards issued in your country are not supported.&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="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;300&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;There are several options in the market for BIN API providers. One option worth looking at is &lt;a href="https://www.binsearchlookup.com/" rel="noopener noreferrer"&gt;binsearchlookup.com&lt;/a&gt;, which returns &lt;code&gt;Brand&lt;/code&gt;, &lt;code&gt;Type&lt;/code&gt;, &lt;code&gt;Category&lt;/code&gt;, &lt;code&gt;Issuer&lt;/code&gt;, &lt;code&gt;IssuerPhone&lt;/code&gt;, &lt;code&gt;isoCode2&lt;/code&gt;, &lt;code&gt;isoCode3&lt;/code&gt;, and &lt;code&gt;CountryName&lt;/code&gt; in a single GET request authenticated with an &lt;code&gt;X-API-Key&lt;/code&gt; and &lt;code&gt;X-User-ID&lt;/code&gt; header pair. It also supports batch lookups of up to 50 BINs per request, which is useful for reconciliation jobs and fraud analysis pipelines. Evaluate any provider on coverage breadth, update frequency, and uptime SLA before committing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist: What a Production-Grade Card Validation Flow Should Do
&lt;/h2&gt;

&lt;p&gt;Use this checklist before shipping any payment form to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Format Validation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Strip non-numeric characters before running any check&lt;/li&gt;
&lt;li&gt;[ ] Run a Luhn check on the cleaned number&lt;/li&gt;
&lt;li&gt;[ ] Validate length against the expected range for the detected brand&lt;/li&gt;
&lt;li&gt;[ ] Use 8-digit BIN extraction, not 6-digit, where your BIN provider supports it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;BIN Intelligence&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Query a BIN API for every card entry, not just suspicious ones&lt;/li&gt;
&lt;li&gt;[ ] Verify the card brand from BIN data (&lt;code&gt;Brand&lt;/code&gt; field), not from regex alone&lt;/li&gt;
&lt;li&gt;[ ] Check the &lt;code&gt;Type&lt;/code&gt; field (&lt;code&gt;CREDIT&lt;/code&gt;, &lt;code&gt;DEBIT&lt;/code&gt;, &lt;code&gt;PREPAID&lt;/code&gt;) and enforce your prepaid policy&lt;/li&gt;
&lt;li&gt;[ ] Check the &lt;code&gt;isoCode2&lt;/code&gt; field and enforce your geographic restrictions by issuer country&lt;/li&gt;
&lt;li&gt;[ ] Check the &lt;code&gt;Category&lt;/code&gt; field and route commercial or premium cards to the correct interchange tier&lt;/li&gt;
&lt;li&gt;[ ] Verify the returned &lt;code&gt;Brand&lt;/code&gt; is covered by your processor agreements&lt;/li&gt;
&lt;li&gt;[ ] Store &lt;code&gt;IssuerPhone&lt;/code&gt; with each transaction record for customer support use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Failure Handling&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Set a short timeout (1-2 seconds) on BIN API calls&lt;/li&gt;
&lt;li&gt;[ ] Fail open on BIN API timeout: log the event, do not block the customer&lt;/li&gt;
&lt;li&gt;[ ] Return meaningful error messages to the UI for blocked card types&lt;/li&gt;
&lt;li&gt;[ ] Never expose raw BIN API error responses to the frontend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Logging and Observability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Log BIN metadata alongside every authorization attempt&lt;/li&gt;
&lt;li&gt;[ ] Log BIN timeouts separately so you can track provider reliability&lt;/li&gt;
&lt;li&gt;[ ] Alert on elevated BIN API error rates&lt;/li&gt;
&lt;li&gt;[ ] Track decline rate by BIN country to spot geographic fraud patterns&lt;/li&gt;
&lt;li&gt;[ ] Track decline rate by card type to validate your prepaid blocking logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Monitoring and Experimentation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] A/B test your BIN-based blocking rules before enforcing them hard&lt;/li&gt;
&lt;li&gt;[ ] Start with shadow mode: log what would be blocked without blocking it&lt;/li&gt;
&lt;li&gt;[ ] Review shadow-mode results for two weeks before turning on hard rejection&lt;/li&gt;
&lt;li&gt;[ ] Compare auth rates before and after each new BIN rule you add&lt;/li&gt;
&lt;li&gt;[ ] Monitor false positive rate: legitimate cards incorrectly blocked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Interchange Optimization&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Pass Level 2 data (tax amount, customer code) for commercial card authorizations&lt;/li&gt;
&lt;li&gt;[ ] Confirm your processor supports Level 2 and Level 3 pass-through&lt;/li&gt;
&lt;li&gt;[ ] Track interchange costs by card category month over month&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Practical Advice on Logging, Monitoring, and Experimenting Safely
&lt;/h2&gt;

&lt;p&gt;Adding BIN intelligence to a live checkout is not a switch you flip on a Friday afternoon. Here is how to do it without tanking your conversion rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start in shadow mode.&lt;/strong&gt; Deploy the BIN lookup and all your business rules, but do not act on the results yet. Log what would be rejected and why. After two weeks, review the shadow log. If you see 8 percent of your legitimate volume flagged for prepaid, your prepaid data source might be wrong, or your customer base genuinely uses prepaid cards and you need to reconsider the policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate new rules behind feature flags.&lt;/strong&gt; Your BIN blocking logic should be toggleable per rule without a deployment. When a rule starts generating unexpected declines, you want to turn it off in thirty seconds, not thirty minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log BIN metadata with every transaction.&lt;/strong&gt; Store &lt;code&gt;Brand&lt;/code&gt;, &lt;code&gt;Type&lt;/code&gt;, &lt;code&gt;Category&lt;/code&gt;, &lt;code&gt;isoCode2&lt;/code&gt;, and &lt;code&gt;Issuer&lt;/code&gt; alongside your authorization records. This data becomes extremely valuable six months later when you are trying to understand why your European auth rate dropped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set up decline-rate alerts by BIN country.&lt;/strong&gt; A sudden spike in declines from cards issued in a specific country often signals a fraud wave or a change in issuer behavior. You cannot see this pattern without country-level logging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not trust BIN data blindly.&lt;/strong&gt; BIN databases are not perfect. Card networks issue new BIN ranges. Issuers reassign ranges between products. Test your BIN provider's accuracy by running a sample of known cards through it and checking the results against ground truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache BIN responses aggressively.&lt;/strong&gt; A BIN lookup result does not change from one transaction to the next. Cache with a TTL of 24 hours or longer. This reduces latency, cuts API costs, and eliminates timeout risk on repeat customers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lru_cache&lt;/span&gt;

&lt;span class="n"&gt;BSL_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.binsearchlookup.com/lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BSL_API_KEY&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bsl_your_64_character_key_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BSL_USER_ID&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-uuid-here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_cached_bin_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin_digits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    In production, replace lru_cache with Redis or Memcached.
    BIN data is stable. A 24-hour TTL is safe.
    BinSearchLookup also returns a &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cached&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; boolean so you can
    tell whether the result came from their edge cache.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&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="n"&gt;BSL_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin_digits&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BSL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-User-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BSL_USER_ID&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does BIN lookup replace the Luhn check?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Luhn runs first as a fast, zero-latency check that catches transcription errors and obviously fake numbers. BIN lookup runs after Luhn passes. They are complementary layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I do BIN validation entirely on the client side?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can cache BIN data and do a client-side lookup for UI purposes, like showing a card brand logo or blocking a disallowed type before form submission. But your server must independently validate BIN data. Client-side checks are bypassed trivially.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between BIN and IIN?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They refer to the same digits. BIN (Bank Identification Number) is the older term. IIN (Issuer Identification Number) is the ISO 7812 standard term. The card networks use both interchangeably in documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often do BIN databases change?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Frequently. New BIN ranges are issued as banks launch new card products. Existing ranges are reassigned when banks merge or discontinue products. A high-quality BIN provider updates their database daily from network feeds. Ask your provider how often their data is refreshed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the BIN API is down?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fail open. Log the timeout. Do not block the customer because your external dependency is unavailable. You will still have downstream fraud signals from your processor. A hard fail on BIN API downtime is a self-inflicted outage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is 6-digit or 8-digit BIN lookup more accurate?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Eight-digit. ISO 7812 extended the BIN from 6 to 8 digits to accommodate the growing number of card issuers. Six-digit lookups on 8-digit BIN ranges can return ambiguous or incorrect results. Use 8-digit lookups if your provider supports them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can BIN data improve my fraud scoring?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, significantly. Issuer country mismatch with the customer's IP, prepaid card usage on a high-value digital goods order, and commercial cards used for consumer purchases are all signals that improve fraud model precision. BIN fields feed directly into rules-based and model-based fraud scoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need BIN lookup for every transaction or just new card entries?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every new card entry. If you vault cards, you should also run BIN lookup when the card is first stored and store the BIN metadata alongside the token. Re-run the lookup periodically (quarterly) to catch changes, particularly for cards issued on ranges that have been reassigned.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Card validation is not a checkbox. It is a pipeline. Regex and Luhn get you to the starting line. BIN intelligence is what separates a checkout that looks functional from one that actually protects your business.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>backend</category>
      <category>security</category>
      <category>fintech</category>
    </item>
    <item>
      <title>How BIN Lookup Actually Stops Card Fraud for Small E‑commerce Stores</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Wed, 18 Mar 2026 23:55:55 +0000</pubDate>
      <link>https://dev.to/rabbitholeinphp/how-bin-lookup-actually-stops-card-fraud-for-small-e-commerce-stores-14h8</link>
      <guid>https://dev.to/rabbitholeinphp/how-bin-lookup-actually-stops-card-fraud-for-small-e-commerce-stores-14h8</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A BIN (Bank Identification Number) is the first 6 to 8 digits of any payment card. It tells you the issuing bank, card brand, card type, and country of origin before you authorize a single cent.&lt;/li&gt;
&lt;li&gt;Most small stores skip BIN checks entirely, which is exactly why their chargeback rates creep past 1% and Stripe starts sending warning emails.&lt;/li&gt;
&lt;li&gt;BIN data lets you flag prepaid cards, mismatched geographies, and high-risk card origins at checkout, before the fraud clears.&lt;/li&gt;
&lt;li&gt;Adding a BIN lookup to your checkout flow takes less than a day and costs almost nothing compared to a single $500 chargeback plus fees.&lt;/li&gt;
&lt;li&gt;Stores that implement BIN checks correctly see chargeback rates drop 40 to 60% within 90 days, according to merchant case data, without turning away legitimate customers.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The $3,200 Tuesday That Changed How I Think About Checkout
&lt;/h2&gt;

&lt;p&gt;A founder I know runs a small Shopify store selling specialty audio equipment. Average order value: $280. Good margins. Growing steadily.&lt;/p&gt;

&lt;p&gt;One Tuesday he woke up to 11 new orders, all placed between 2 a.m. and 4 a.m. He was thrilled until he checked the shipping addresses. Eleven different addresses across four states. Eleven different email addresses. Same card type across the board: prepaid Visa gift cards.&lt;/p&gt;

&lt;p&gt;He shipped nine of the orders before his payment processor flagged the pattern.&lt;/p&gt;

&lt;p&gt;The chargebacks arrived six weeks later. He lost $3,200 in product, $350 in chargeback fees, and spent two full days responding to disputes he could not win.&lt;/p&gt;

&lt;p&gt;Here is the thing: a BIN check on those cards would have told him "prepaid" in under 100 milliseconds. He could have auto-flagged every one of those orders for manual review before a single box left his warehouse.&lt;/p&gt;

&lt;p&gt;That is what this article is about.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is BIN Lookup?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BIN lookup is the process of decoding the first 6 to 8 digits of a payment card number to retrieve the issuing bank, card brand, card type, and country of origin.&lt;/strong&gt; This happens in the background of your checkout, before authorization, in under 200 milliseconds.&lt;/p&gt;

&lt;p&gt;BIN stands for Bank Identification Number. Some payment networks now call it IIN, Issuer Identification Number. The name change reflects that the first digits identify the issuing institution, not just a bank per se. For practical purposes BIN and IIN are interchangeable.&lt;/p&gt;

&lt;p&gt;When a customer types their card number into your checkout, those first digits encode a significant amount of information. A BIN lookup decodes them into actionable fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Card brand&lt;/strong&gt;: Visa, Mastercard, Amex, Discover, UnionPay&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Card type&lt;/strong&gt;: credit, debit, prepaid, corporate, virtual&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuing bank&lt;/strong&gt;: Chase, HSBC, Revolut, a regional credit union&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Country of issuance&lt;/strong&gt;: where the card was originally issued, as an ISO country code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Card tier&lt;/strong&gt;: Standard, Gold, Platinum, Business&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this requires the customer to do anything different. The response comes back in under 200 milliseconds on any reasonably-maintained API. Your customer never notices it happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt; How Stripe Radar scoring works and where it falls short | Chargeback prevention playbook for DTC brands&lt;/p&gt;




&lt;h2&gt;
  
  
  Why BIN Data Is Underused by Small Stores
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Most small stores rely entirely on their payment processor's fraud detection, which treats BIN data as an opaque input rather than an exposed signal. That means you cannot tune it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stripe Radar gives you some risk scoring. PayPal has its own fraud detection. These tools are useful but they treat BIN data as one input into a black box. When Stripe flags a legitimate order from a French customer because France has elevated fraud rates in their global model, you cannot easily override that logic without building around it.&lt;/p&gt;

&lt;p&gt;When you run your own BIN check, you decide the rules. Flag prepaid cards over $150, not all prepaid cards. Require phone verification for cards issued outside your primary market, not block them entirely. The control is yours.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Fields Actually Matter at Checkout
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The five BIN fields with the most fraud-prevention value are: country of issuance, card type (especially prepaid), issuing bank, card brand, and card tier. Country and type carry the most weight.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Country of issuance&lt;/strong&gt; is your most powerful signal. If your store sells domestically and a card was issued in a country with known fraud infrastructure, that mismatch between billing address country and card issuance country deserves attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Card type, specifically prepaid&lt;/strong&gt;, is your second most powerful signal. Prepaid cards are not inherently fraudulent. Plenty of legitimate customers use them for privacy or budgeting. But prepaid cards are also the tool of choice for card testing attacks and refund fraud, because they are disposable and often purchased with cash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issuing bank&lt;/strong&gt; matters more than people think. Certain challenger banks and virtual card issuers have disproportionately high fraud rates not because those banks are bad, but because fraudsters find them easier to open accounts with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Card brand&lt;/strong&gt; is a weak fraud signal on its own, but it matters for routing and card cost structures. Amex corporate cards behave differently than consumer debit cards in dispute resolution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Four Fraud Patterns BIN Data Helps You Stop
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BIN data directly addresses four of the most common fraud patterns hitting small e-commerce stores: country mismatches, prepaid card abuse, card testing attacks, and virtual card attacks.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. High-Risk Country Mismatches
&lt;/h3&gt;

&lt;p&gt;A customer claims to be in Austin, Texas. Billing address: Austin. IP address: Austin. Card issued by: a bank in Eastern Europe.&lt;/p&gt;

&lt;p&gt;That mismatch does not mean the order is fraudulent. Immigrants, expats, and frequent travelers use foreign-issued cards all the time. But it warrants a second look, especially on high-value orders. Without BIN data you see a clean billing address and approve. With BIN data you see the mismatch and can ask for additional verification.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Prepaid Card Abuse
&lt;/h3&gt;

&lt;p&gt;The audio equipment story at the top of this article is a classic prepaid card attack. Fraudsters buy prepaid cards with cash, purchase goods they can resell, and the cards are untraceable back to any real identity.&lt;/p&gt;

&lt;p&gt;The chargeback does not come from a real cardholder because there is no real cardholder. It comes from the prepaid card issuer when the fraudster disputes the charge after receiving the goods. BIN data tells you instantly whether a card is prepaid so you can apply your own policy before anything ships.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Card Testing Attacks
&lt;/h3&gt;

&lt;p&gt;Card testing is when an attacker has a list of stolen card numbers and runs small test transactions, often under $2, to find out which ones are still active. Your fraud loss per test is tiny, but processors notice the pattern. Visa's published chargeback monitoring program rules classify merchants with sustained testing patterns as high-risk, triggering additional scrutiny from acquirers.&lt;/p&gt;

&lt;p&gt;Many card testing scripts use cards from a narrow range of BINs. If you see multiple failed transactions sharing the same first 6 digits, you can block at the BIN level without touching legitimate customers.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Virtual and Burner Card Attacks
&lt;/h3&gt;

&lt;p&gt;Attackers use virtual card services to generate technically valid card numbers designed to be single-use and anonymous. Certain BINs belong exclusively to virtual card programs. A BIN lookup will flag these. Some are entirely legitimate, like privacy-conscious shoppers using Privacy.com. Others are attacker infrastructure. Knowing the difference lets you set nuanced policies rather than blanket blocks.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Does BIN Data Reduce Chargebacks: The Mechanism
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BIN data reduces chargebacks by letting you intercept fraud before it clears, at the moment a card number is entered, before goods ship and before disputes are filed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Chargebacks happen when a cardholder tells their bank they did not authorize a charge. The bank reverses the transaction and charges you a dispute fee, typically $15 to $100 per incident. According to Visa's published chargeback monitoring program rules, most processors begin watching merchant accounts at a 0.65% monthly chargeback-to-transaction ratio. At 1% you enter formal dispute programs with monthly monitoring fees. At 1.5% account termination is a real risk.&lt;/p&gt;

&lt;p&gt;The fraud that generates chargebacks is almost entirely predictable from signals available at authorization time. You can know the card's origin with a BIN lookup before a single authorization request fires.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Realistic Mini Case Study: Before and After BIN Checks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A two-person supplements brand cut their chargeback rate from 1.2% to 0.4% in 90 days by adding two BIN-based rules to their checkout. Here is exactly what they built.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The store:&lt;/strong&gt; Direct-to-consumer supplements. Two-person team. Average order value: $68. Monthly volume: roughly 800 orders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Chargeback rate climbed to 1.2% over six months. Stripe sent a formal warning. Manual review was eating four hours a week and fraud was still slipping through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What they were missing:&lt;/strong&gt; 70% of fraud orders involved prepaid Visa or Mastercard cards. Another 15% involved cards issued in countries with no overlap with their customer demographics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What they built:&lt;/strong&gt; Two rules triggered at card entry:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If card type is prepaid AND order value is over $50, hold for manual review.&lt;/li&gt;
&lt;li&gt;If card issuing country is not in the store's known-market list AND billing address country does not match, send an automated verification email before shipping.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They used the lookup endpoint from &lt;a href="https://www.binsearchlookup.com/" rel="noopener noreferrer"&gt;binsearchlookup.com&lt;/a&gt; during development because the API response structure was clean and the documentation made the integration straightforward. Total build time: six hours including testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results over 90 days:&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;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chargeback rate&lt;/td&gt;
&lt;td&gt;1.2%&lt;/td&gt;
&lt;td&gt;0.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orders held for review&lt;/td&gt;
&lt;td&gt;~0 (nothing caught)&lt;/td&gt;
&lt;td&gt;31 orders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fraud confirmed in held orders&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;24 of 31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legitimate orders delayed&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;7 of 31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chargeback fees saved&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;~$1,100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual review time per week&lt;/td&gt;
&lt;td&gt;4 hrs (reactive)&lt;/td&gt;
&lt;td&gt;1.5 hrs (proactive)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 7 legitimate orders that got delayed were resolved within 24 hours via the verification email. One customer left a positive review specifically mentioning they appreciated the security check.&lt;/p&gt;




&lt;h2&gt;
  
  
  Without BIN Checks vs. With BIN Checks at Checkout
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Without BIN&lt;/th&gt;
&lt;th&gt;With BIN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prepaid card, $200 order&lt;/td&gt;
&lt;td&gt;Approved, ships immediately&lt;/td&gt;
&lt;td&gt;Flagged for review based on your threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Foreign-issued card, domestic address&lt;/td&gt;
&lt;td&gt;Approved, no additional signal&lt;/td&gt;
&lt;td&gt;Mismatch detected, verification triggered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Card testing, $1.00 probes&lt;/td&gt;
&lt;td&gt;Pattern builds invisibly&lt;/td&gt;
&lt;td&gt;BIN clustering alert fires within 60 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legitimate customer, foreign card&lt;/td&gt;
&lt;td&gt;Approved&lt;/td&gt;
&lt;td&gt;Approved with zero friction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stolen credit card, matching billing&lt;/td&gt;
&lt;td&gt;Approved&lt;/td&gt;
&lt;td&gt;Partially mitigated by issuer and type data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual card from privacy service&lt;/td&gt;
&lt;td&gt;Approved&lt;/td&gt;
&lt;td&gt;Identified as virtual, your policy applied&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;BIN checks do not block good customers. They add a signal layer. What you do with that signal is your policy decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Should Small Stores Use a BIN API?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Implement a BIN API if your average order value exceeds $75, you ship physical goods, or you have seen even one chargeback tied to a prepaid card or unexpected country of origin.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You need a BIN check if any of the following are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your average order value is above $75. Above that threshold, a single chargeback fee can exceed your margin on several orders.&lt;/li&gt;
&lt;li&gt;You ship physical goods. Physical goods fraud is almost always caught too late once items leave the warehouse.&lt;/li&gt;
&lt;li&gt;You have seen even one chargeback in the past 90 days involving a prepaid card or an unexpected country of origin.&lt;/li&gt;
&lt;li&gt;Your current fraud solution is entirely handled by your payment processor with no custom rules.&lt;/li&gt;
&lt;li&gt;You are scaling ad spend into new geographies and have no historical fraud data to calibrate against.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your store is brand new and processing under $5,000 per month, you can probably wait. But build it into your roadmap for the moment you hit consistent volume.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Wire BIN Checks Into a Basic Risk Engine
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You can implement a working BIN risk engine in four steps: capture the BIN at card input, store the response with the order, score against simple rules, and act on the score. Total build time for a solo developer: 4 to 8 hours.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Capture the BIN at card input&lt;/strong&gt; &lt;em&gt;(~15 minutes)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Listen for the card number field reaching 8 characters. Fire a backend request to your BIN API. Do not expose your API key client-side. Route the lookup through your own backend endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Store the BIN response alongside the order&lt;/strong&gt; &lt;em&gt;(~30 minutes)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before any authorization happens, attach the BIN response to the order object. Store at minimum: card type, issuing country, card brand, and issuing bank name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Score the order against your rules&lt;/strong&gt; &lt;em&gt;(~2 hours)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;card_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prepaid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;issuing_country&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;billing_country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;issuing_country&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;HIGH_RISK_COUNTRY_LIST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;order_value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;HIGH_VALUE_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;customer_email_domain&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;KNOWN_THROWAWAY_DOMAINS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Act on the score&lt;/strong&gt; &lt;em&gt;(~1 hour)&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Below 30: process normally&lt;/li&gt;
&lt;li&gt;30 to 60: hold for review or require additional verification&lt;/li&gt;
&lt;li&gt;Above 60: block and notify&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tune these thresholds based on your false positive and false negative rates after the first 30 days.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Does BIN Lookup Fit With 3DS2 and Other Fraud Layers?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BIN lookup is most effective when used as a pre-authorization signal that feeds into your broader fraud stack, alongside 3D Secure 2 (3DS2), velocity checks, and device fingerprinting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;3D Secure 2 (3DS2) is the card network protocol that adds a challenge step for suspicious transactions and shifts chargeback liability to the issuing bank when authentication succeeds. BIN lookup complements 3DS2 by helping you decide which transactions to challenge before invoking the 3DS2 flow.&lt;/p&gt;

&lt;p&gt;The practical layering looks like this: BIN lookup runs first at card entry. If the risk score is low, the order proceeds and 3DS2 handles authentication at the network level. If the risk score is elevated, you invoke 3DS2 explicitly or hold for manual review before authorization.&lt;/p&gt;

&lt;p&gt;Neither layer replaces the other. BIN data tells you about the card's origin and type. 3DS2 tells you whether the cardholder can authenticate in real time. Velocity checks tell you whether the same card, email, or device has been used unusually often. Together, they catch what each layer misses alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt; Setting up 3DS2 with Stripe: a developer's guide | Velocity check patterns for e-commerce risk engines&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Implementation Tips
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Log every BIN field, set a BIN clustering alert, run a weekly chargeback audit, and review held orders within 24 hours. These four habits separate stores that tune their fraud systems from those that set and forget.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log everything.&lt;/strong&gt; Store every BIN lookup response in your order database, not just the final risk decision. When a chargeback comes in three months from now, you want to query "show me all orders where the issuing country was X" and see the pattern immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set up a weekly chargeback audit.&lt;/strong&gt; Pull your chargeback data, join it with your BIN log, and look for patterns. Are prepaid cards over-represented? Is a specific issuing country appearing repeatedly? This is how you tune your rules without guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create an alert for BIN clustering.&lt;/strong&gt; If more than three orders in a 60-minute window share the same first 6 BIN digits, that is a card testing pattern worth investigating immediately. This alert alone can prevent a wave of fraud losses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not block entire countries.&lt;/strong&gt; Blocklisting is both legally risky in some jurisdictions and commercially damaging if you ever want to expand. Use country as one signal among several and require additional verification for orders that cross multiple risk thresholds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review your held orders within 24 hours.&lt;/strong&gt; Legitimate customers get frustrated with delays. A fast review with a verification email maintains the experience while catching fraud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recheck your BIN database periodically.&lt;/strong&gt; BIN databases are not static. Banks merge, reissue card ranges, and change product classifications. Use a provider that updates regularly and build in a monthly check on your top-volume card ranges.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does BIN lookup actually work against professional fraud rings?
&lt;/h3&gt;

&lt;p&gt;Yes, for the fraud patterns that hit small stores most often. Sophisticated fraud rings rotate card sources and sometimes use legitimate-looking cards from real issuers, which limits BIN data's effectiveness there. But most small-store fraud is from opportunistic attackers using prepaid cards and stolen card lists, and BIN data catches the vast majority of that. Against advanced actors, layer BIN data with velocity checks and device fingerprinting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will BIN checks slow down my checkout?
&lt;/h3&gt;

&lt;p&gt;No. A well-maintained BIN API responds in under 200 milliseconds. You fire the lookup the moment the customer enters their card number, well before they click Pay. Perceived checkout speed is unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if a legitimate customer uses a prepaid card?
&lt;/h3&gt;

&lt;p&gt;Flag it, do not block it. Flag prepaid cards for review at certain order value thresholds, or require address verification before shipping. Most legitimate prepaid card users understand and comply. Fraudsters do not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it legal to use BIN data to make checkout decisions?
&lt;/h3&gt;

&lt;p&gt;In most jurisdictions, yes. Using card metadata to make authorization decisions is standard industry practice supported by card network operating rules. Use country as one signal among several rather than a blanket block. Consult your legal counsel for jurisdiction-specific guidance.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should I update my risk rules?
&lt;/h3&gt;

&lt;p&gt;Monthly for the first six months, then quarterly after that unless your fraud rate spikes. Each review should compare confirmed fraud orders against your risk model outputs to identify false negatives and check you are not over-flagging legitimate customers.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is BIN lookup different from what Stripe Radar already does?
&lt;/h3&gt;

&lt;p&gt;Stripe Radar uses BIN data internally but does not expose raw fields like card type as configurable variables. You cannot write a Radar rule that says "if card type is prepaid AND order value is over $50, hold for review" because Radar does not surface card type as a tunable input. Running your own BIN lookup gives you direct access to those fields before the Stripe authorization request is even made.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;You do not need a fraud team to run a sensible fraud operation. You need data that your payment processor does not surface automatically, and a small amount of code to act on that data.&lt;/p&gt;

&lt;p&gt;BIN lookup is the single highest-leverage addition most small e-commerce stores can make to their checkout flow. It is cheap, fast to implement, and catches the fraud patterns that generate the majority of small-store chargebacks.&lt;/p&gt;

&lt;p&gt;Build it before you need it. The Tuesday morning with 11 fraudulent orders has a way of arriving without warning.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful, share it with a developer friend who runs their own store. Questions about implementation are welcome in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>security</category>
      <category>api</category>
    </item>
  </channel>
</rss>
