A card BIN (the first 6–8 digits) tells you which bank issued a card. Most BIN APIs stop there — brand, type, country. I wanted one that returns a decision: given a BIN, approve / review / decline — and why — fast enough to call inline on a checkout.
So I built BillionCore, a BIN risk-scoring engine in Go. Here's what's under the hood.
Decision, not just data
For each BIN the engine returns:
- an approve / review / decline call with the reasons behind it,
- a risk score + issuer data,
- and (for media-buying/affiliate use) rebill probability, refund/chargeback risk and projected LTV.
Every response lists the signals that moved the score — the "why" matters as much as the verdict.
The hot path must be fast — and never block
It runs inline with payment/traffic decisions, so reads have to be sub-millisecond and must never stall, even while data is updating.
The approach: keep the whole ruleset in memory as an immutable snapshot, swapped atomically. Reads are a single atomic load, no mutex (simplified):
type Store struct {
snap atomic.Pointer[Snapshot] // immutable; rebuilt once per reload
}
// Read path — lock-free
func (s *Store) Lookup(bin string) (Rule, bool) {
r, ok := s.snap.Load().rules[bin]
return r, ok
}
// Reload path — build a fresh snapshot, swap in one atomic op
func (s *Store) reload(path string) error {
next, err := buildSnapshot(path)
if err != nil {
return err // keep serving the previous snapshot on failure
}
s.snap.Store(next) // readers never block, never see a half-built map
return nil
}
```
No locks on reads, no torn reads during reload, and a failed reload just keeps the last good snapshot. (Basically RCU — read-copy-update.)
## Hot-reload + a clean data/control-plane split
The data (BIN rules, performance stats, valid keys) changes over time, but I didn't want the fast path depending on a DB or app:
- The **Go engine** is the data plane — stdlib only, in-memory, lock-free.
- A separate **control plane** (a Laravel app) owns keys/rules/billing and writes them to disk atomically (temp file → `rename`).
- The engine **hot-reloads** from disk on an interval (and on `SIGHUP` for the big CSV datasets).
The engine keeps serving even if the control plane is down. Eventually-consistent updates are fine for this data and keep the hot path dependency-free.
## Why stdlib-only
`net/http` + `ServeMux` + the standard library got me sub-ms responses with a tiny footprint and zero dependency churn. For a focused service, a framework would've added weight without buying anything.
## Try it
Live demo, type any BIN, no signup → **https://billioncore.tech/#demo**
On RapidAPI if you want to call it from code → https://rapidapi.com/billioncore-billioncore-default/api/bin-lookup-risk-scoring-billioncore
## Honest notes
It's early. The performance dataset is solid on major issuers/geos and thinner on long-tail BINs — and a "decision" is only as good as the data behind it. Building this in public, so I'd genuinely value feedback on the scoring, the API shape, and where the data looks off.
If you've built low-latency lookup services in Go — how do you handle hot-reloading large datasets? Always looking to sharpen this.
Top comments (0)