Docs menuAll pages, quickstarts and this page’s contents
- Androidsoon
- iOSsoon
- React Nativesoon
- Fluttersoon
Go quickstart
Score a signup from a Go service in about five minutes. The client isgithub.com/kaidn-io/kaidn-go, and it has no dependencies: the whole API is JSON over HTTP, so a dependency would buy nothing and cost you a supply chain to audit.
Every snippet below was run against a live API before it was published, and the outputs are transcripts.
Get your API key
Sign up at kaidn.io. The free tier is 10,000 events a month with no card. From your dashboard you get two keys, and they are not interchangeable.
| Key | Where it belongs | What it can do |
|---|---|---|
kdn_live_… | your server, only | score, read events, erase data |
kdn_pub_… | safe in a browser | submit fingerprints, nothing else |
This page uses the secret key. If you paste a publishable key into a server, /v1/score rejects it, which is the failure you want: the opposite mistake, a secret key in a page, is a breach rather than an error.
export KAIDN_SECRET_KEY="kdn_live_…"
Set up your project
mkdir kaidn-quickstart && cd kaidn-quickstart go mod init example.com/kaidn-quickstart go get github.com/kaidn-io/kaidn-go
That is the only go get on this page. Go 1.21 or later.
Score your first signup
One event in, one verdict out. Only Event is required; everything else is a signal, and each one you add is another check that can fire.
package main import ( "context" "fmt" "log" "os" kaidn "github.com/kaidn-io/kaidn-go" ) func main() { client := kaidn.New(kaidn.Options{APIKey: os.Getenv("KAIDN_SECRET_KEY")}) res, err := client.Score(context.Background(), kaidn.Event{ Event: "signup", UserID: "u_2001", Email: "priya@gmail.com", IP: "24.60.140.22", DeviceID: "device-priya", }) if err != nil { log.Fatal(err) } fmt.Printf("score=%d verdict=%s\n", res.Score, res.Verdict) for _, c := range res.Checks { fmt.Printf(" %-24s %3d %s\n", c.Reason, c.Weight, c.Message) } }
go run .
score=0 verdict=allowNothing fired, so nothing is printed under it. A residential IP, a real mailbox and a device nobody has seen before is what an ordinary customer looks like.
Block bots and datacenter traffic
Change two fields: a hosting IP and a disposable address.
Email: "bot@mailinator.com", IP: "45.83.220.1",
go run . score=80 verdict=block datacenter_ip 45 IP is a datacenter/hosting address (hosting), not a residential user disposable_email 35 Email uses a disposable/temporary domain abusive_email_domain 30 Email domain is on a known-abusive (spam/fraud) list
Three checks fired and their weights sum past the block threshold. Note the score is clamped at 100, so the arithmetic will not always add up on the page: the weights are the honest record of what happened, and the score is bookkeeping.
Checksis the field worth logging. It is the answer to “why was this account blocked”, and having it already in your logs is the difference between a two-minute support reply and an afternoon of guessing.
Stop one person opening many accounts
The dominant abuse is not a bot, it is one person with many addresses. Gmail ignores dots and everything after a plus sign, so a single inbox is an unlimited supply.
// One person, one device, three addresses that all reach one inbox. for i, email := range []string{ "sam.taylor@gmail.com", "samtaylor@gmail.com", "sam.taylor+trial@gmail.com", } { res, _ := client.Score(ctx, kaidn.Event{ Event: "signup", UserID: fmt.Sprintf("u_30%02d", i), Email: email, IP: "24.60.140.22", DeviceID: "device-sam", }) fmt.Printf("%-28s score=%-3d %s\n", email, res.Score, res.Verdict) }
go run . sam.taylor@gmail.com score=15 allow aliased_address 15 Email is a re-spelling of another address that reaches the same inbox samtaylor@gmail.com score=60 review device_reuse 15 This device is linked to 2 accounts, on different networks email_reuse 45 This mailbox is behind 2 accounts sam.taylor+trial@gmail.com score=100 block device_reuse 15 This device is linked to 3 accounts, on different networks ip_velocity 25 5 accounts from this IP in a short window plus_addressing 10 Email uses plus-addressing, common when farming accounts aliased_address 15 Email is a re-spelling of another address that reaches the same inbox email_reuse 55 This mailbox is behind 3 accounts
The escalation is the point. The first attempt is allowed, because one alias is not evidence of anything. By the third the evidence has accumulated from four directions at once and it stops.
Look at device_reuse: it is 15, the smallest weight there. That is deliberate. A raw browser fingerprint is a hash of settings, and identical settings produce identical hashes, so on our own traffic one fingerprint covers 2.30 real people on iOS Safari. A device match that could block on its own would be refusing strangers, and the refusals would land almost entirely on one platform. It is evidence, weighted to matter in company. See device identity.
Wire it into a handler
Three things matter in production, and none of them is the scoring call.
func signup(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
res, err := client.Score(r.Context(), kaidn.Event{
Event: "signup",
UserID: email,
Email: email,
IP: kaidn.ClientIP(r), // 1. the REAL client address
DeviceID: r.FormValue("kaidn_device_id"),
})
// 2. FAIL OPEN. An outage at a fraud vendor must never become an
// outage in your product.
if err != nil {
log.Printf("kaidn unavailable, allowing: %v", err)
createAccount(email)
return
}
// 3. Three outcomes, not two.
switch res.Verdict {
case kaidn.VerdictBlock:
http.Error(w, "could not create that account", http.StatusForbidden)
case kaidn.VerdictReview:
createAccount(email) // create it, hold what is worth stealing
holdTrial(email, res.EventID)
default:
createAccount(email)
}
}| Why it matters | |
|---|---|
kaidn.ClientIP(r) | Behind a load balancer r.RemoteAddr is the balancer. Every user then shares one address, the IP checks correlate everyone with everyone, and you conclude the product is broken. |
| Fail open | The client already retried the transient cases, so an error reaching you means the service is genuinely unreachable. Allow and flag; never refuse the user. |
| Three verdicts | review is where a hold, a verification step or a human look is proportionate. Collapsing it forces every uncertain signup into a false positive or a free pass. |
Next steps
The client covers all 23 endpoints. Two worth knowing about early:
// Is this address disposable? No event scored, nothing billed. e, _ := client.CheckEmail(ctx, "a.b+tag@mailinator.com") e.Email.IsDisposable // true e.Email.Canonical // dots and plus-tags stripped // What has this device actually been seen doing? The call that makes a // device match interpretable rather than merely true. obs, _ := client.DeviceObservations(ctx, deviceID) // Backfill: one call, not one per row. out, _ := client.BatchScore(ctx, rows)
- A full worked example with a browser collector and a demo repo you can clone.
- Package reference on pkg.go.dev
- Device identity: the three resolution rungs and the measured collision risk of each.