Docs menuAll pages, quickstarts and this page’s contents

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.

01

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.

KeyWhere it belongsWhat it can do
kdn_live_…your server, onlyscore, read events, erase data
kdn_pub_…safe in a browsersubmit 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.

Terminal
export KAIDN_SECRET_KEY="kdn_live_…"
02

Set up your project

Terminal
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.

03

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.

main.go
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)
	}
}
Terminal
go run .

score=0 verdict=allow

Nothing 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.

04

Block bots and datacenter traffic

Change two fields: a hosting IP and a disposable address.

main.go
		Email:    "bot@mailinator.com",
		IP:       "45.83.220.1",
Terminal
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.

05

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.

main.go
	// 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)
	}
Terminal
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.

06

Wire it into a handler

Three things matter in production, and none of them is the scoring call.

handler.go
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 openThe 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 verdictsreview 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:

more.go
// 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)