kaidn

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 13 Imported by: 0

README

kaidn-go

The server-side Go client for the Kaidn fraud scoring API.

Go Reference

Send one event, get back a verdict and the evidence behind it.

client := kaidn.New(kaidn.Options{APIKey: os.Getenv("KAIDN_SECRET_KEY")})

res, err := client.Score(ctx, kaidn.Event{
    Event:    "signup",
    Email:    email,
    IP:       kaidn.ClientIP(r),
    DeviceID: deviceID,

    // Attribution, when you have it. This is what makes affiliate fraud
    // visible: one identity converting under three partners in a week is
    // not three customers, and no single partner's data can see that.
    Affiliate: affiliateID,
    Campaign:  campaign,
})
{
  "score": 80,
  "verdict": "block",
  "reasons": ["datacenter_ip", "disposable_email"],
  "reason_text": "Strong indicators of abuse: IP is a datacenter/hosting address …"
}

Install

go get github.com/kaidn-io/kaidn-go

No dependencies. The whole API is JSON over HTTP, so a dependency here would buy nothing and cost you a supply chain to audit.

Use it

package main

import (
    "context"
    "log"
    "net/http"
    "os"

    kaidn "github.com/kaidn-io/kaidn-go"
)

func main() {
    client := kaidn.New(kaidn.Options{APIKey: os.Getenv("KAIDN_SECRET_KEY")})

    http.HandleFunc("/signup", func(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),
            DeviceID: r.FormValue("kaidn_device_id"),
        })

        // 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
        }

        switch res.Verdict {
        case kaidn.VerdictBlock:
            http.Error(w, "could not create that account", http.StatusForbidden)
        case kaidn.VerdictReview:
            createAccount(email)   // but hold the thing worth stealing
            holdTrial(email, res.EventID)
        default:
            createAccount(email)
        }
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Three things worth getting right

Fail open. A non-nil error means the service is genuinely unreachable, and the client has already retried the transient cases. The correct response is to allow the action and flag it, never to refuse the user. Any integration that blocks signups when the vendor is down has traded one problem for a worse one.

Use ClientIP. Behind a load balancer, r.RemoteAddr is the balancer. Every user then shares one address, every IP check correlates everyone with everyone, and the product looks broken. ClientIP reads the forwarded headers first. Only trust it when a proxy you control sets them.

Three verdicts, not two. review is the band where a hold, a verification step or a human look is proportionate. Collapsing it into allow-or-block forces every uncertain event into either a false positive or a free pass.

The device id is evidence, not proof

A raw browser fingerprint is a hash of settings, and identical settings produce identical hashes. A default iPhone genuinely matches another default iPhone. On our own traffic one raw fingerprint covers 2.30 real people on iOS Safari.

Every response carries a Device with the resolution method and a CollisionRisk, so you can branch on it:

if d := res.Device; d != nil && d.Resolution == "probabilistic" && d.CollisionRisk > 0.15 {
    // a device-only match on a high-collision identity is a flag, not a block
    return allowWithFlag(res.EventID)
}

Everything else

All 23 endpoints are covered. The scoring loop is what most integrations use; the rest is here when you need it.

Scoring Score Label Forget Events Stats Health
Checks CheckEmail CheckIP CheckPhone Check
Lists Lists AddListEntry DeleteListEntry
Tuning Config SetConfig
Identity DeviceObservations Subject Suppressions
Intel IPInfo Lookups
Batch BatchScore BatchCheckEmail BatchCheckIP BatchCheckPhone BatchLists

Two worth knowing about:

// 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: one inbox, many addresses

// What has this device actually been seen doing? The call that makes a
// device match interpretable rather than just true.
obs, _ := client.DeviceObservations(ctx, deviceID)

Batch is the backfill path: scoring a year of history one row at a time is thousands of round trips for a job that is one call here.

Errors

Every failure is an *Error:

var kerr *kaidn.Error
if errors.As(err, &kerr) {
    switch {
    case kerr.QuotaExceeded():  // 429, out of events for the period
    case kerr.Retryable():      // network, 5xx: worth another go later
    default:                    // 4xx: bad input or bad key, will not improve
    }
}

The client already retries retryable failures twice with backoff, honouring your context. Set Retries to change that; -1 disables it.

Configuration

Option Default
APIKey required Your secret key. Never ship it to a browser.
BaseURL https://api.kaidn.io Point at a local API in tests.
Timeout 5s Per attempt. Short on purpose: scoring is in the critical path.
Retries 2 Retries after the first attempt. -1 for none.
HTTPClient Your own client. Overrides Timeout.

Getting a key

Sign up at kaidn.io. The free tier is 10,000 events a month with no card. You get two keys: a publishable key (kdn_pub_…) for the browser collector, which can only submit fingerprints, and a secret key for your server, which is the one this package wants.

Licence

MIT

Documentation

Overview

Package kaidn is the server-side Go client for the Kaidn fraud scoring API.

It has no dependencies outside the standard library, deliberately: the whole API is JSON over HTTP, so a dependency here would buy nothing and cost every user a supply chain to audit.

client := kaidn.New(kaidn.Options{APIKey: os.Getenv("KAIDN_SECRET_KEY")})

res, err := client.Score(ctx, kaidn.Event{
    Event:    "signup",
    Email:    email,
    IP:       kaidn.ClientIP(r),
    DeviceID: deviceID,
})
if err != nil {
    // fail open: see the note on Score
    log.Printf("kaidn unavailable: %v", err)
    return allow()
}
switch res.Verdict {
case kaidn.VerdictBlock:  return refuse(res)
case kaidn.VerdictReview: return holdForReview(res)
default:                  return allow()
}

Index

Examples

Constants

View Source
const (
	VerdictAllow  = "allow"
	VerdictReview = "review"
	VerdictBlock  = "block"
)

Verdict values returned by Score.

View Source
const Version = "0.2.0"

Version is reported in the User-Agent so API-side issues can be traced to a client release.

Variables

This section is empty.

Functions

func Bool added in v0.2.0

func Bool(v bool) *bool

Bool is a helper for the pointer fields on DeviceSignals:

Device: &kaidn.DeviceSignals{IsHeadless: kaidn.Bool(true)}

func ClientIP

func ClientIP(r *http.Request) string

ClientIP extracts the end user's address from a request.

This exists because getting it wrong is the most common way a Kaidn integration silently stops working. Behind a load balancer r.RemoteAddr is the balancer, so every user shares one address, every IP check correlates everyone with everyone, and the product looks broken.

Only trust the forwarded headers when a proxy YOU control sets them. A client can send X-Forwarded-For itself, so on a directly exposed server this is attacker-controlled input.

Example
package main

import (
	"fmt"
	"net/http"

	kaidn "github.com/kaidn-io/kaidn-go"
)

func main() {
	// Behind a load balancer r.RemoteAddr is the balancer, so every user would
	// share one address and every IP check would correlate everyone with
	// everyone. Only trust the headers when a proxy you control sets them.
	http.HandleFunc("/signup", func(w http.ResponseWriter, r *http.Request) {
		ip := kaidn.ClientIP(r)
		fmt.Fprintln(w, ip)
	})
}

func Int added in v0.2.0

func Int(v int) *int

Int is the same for AntidetectScore.

Types

type Check

type Check struct {
	Check    string         `json:"check"`
	Key      string         `json:"key"`
	Weight   int            `json:"weight"`
	Reason   string         `json:"reason"`
	Message  string         `json:"message"`
	Evidence map[string]any `json:"evidence,omitempty"`
}

Check is one signal that fired, with the weight it contributed to the score.

This is the field worth logging. "Why was this account blocked" is answered here, and having it already in your logs is the difference between a two-minute support reply and an afternoon of guessing.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is safe for concurrent use by multiple goroutines.

func New

func New(opts Options) *Client

New builds a Client. It does not perform any I/O, so it cannot fail; an invalid key surfaces on the first call as an *Error with Status 401.

Example (Options)
package main

import (
	"os"
	"time"

	kaidn "github.com/kaidn-io/kaidn-go"
)

func main() {
	client := kaidn.New(kaidn.Options{
		APIKey:  os.Getenv("KAIDN_SECRET_KEY"),
		Timeout: 2 * time.Second, // tighter: this is in a checkout path
		Retries: 1,
	})
	_ = client
}

func (*Client) AddListEntry added in v0.2.0

func (c *Client) AddListEntry(ctx context.Context, e ListEntry) (map[string]any, error)

AddListEntry adds one allowlist or blocklist rule.

func (*Client) BatchCheckEmail added in v0.2.0

func (c *Client) BatchCheckEmail(ctx context.Context, emails []string) (map[string]any, error)

BatchCheckEmail checks many addresses in one request.

func (*Client) BatchCheckIP added in v0.2.0

func (c *Client) BatchCheckIP(ctx context.Context, ips []string) (map[string]any, error)

BatchCheckIP checks many addresses in one request.

func (*Client) BatchCheckPhone added in v0.2.0

func (c *Client) BatchCheckPhone(ctx context.Context, phones []string) (map[string]any, error)

BatchCheckPhone checks many numbers in one request.

func (*Client) BatchLists added in v0.2.0

func (c *Client) BatchLists(ctx context.Context, entries []ListEntry) (map[string]any, error)

BatchLists adds many list entries in one request.

func (*Client) BatchScore added in v0.2.0

func (c *Client) BatchScore(ctx context.Context, rows []Event) (map[string]any, error)

BatchScore scores many events in one request.

func (*Client) Check added in v0.2.0

func (c *Client) Check(ctx context.Context, email, ip string) (map[string]any, error)

Check inspects an email and an IP together in one call.

func (*Client) CheckEmail added in v0.2.0

func (c *Client) CheckEmail(ctx context.Context, email string) (*EmailCheckResponse, error)

CheckEmail inspects one address: disposable, aliased, gibberish, MX.

func (*Client) CheckIP added in v0.2.0

func (c *Client) CheckIP(ctx context.Context, ip string) (*IPCheckResponse, error)

CheckIP inspects one address: datacenter, proxy, VPN, Tor, ASN, geolocation.

func (*Client) CheckPhone added in v0.2.0

func (c *Client) CheckPhone(ctx context.Context, phone, country string) (*PhoneCheckResponse, error)

CheckPhone inspects one number. Country is an optional ISO-3166 alpha-2 hint for parsing a number that is not in E.164.

func (*Client) Config added in v0.2.0

func (c *Client) Config(ctx context.Context) (map[string]any, error)

Config returns the tenant's weight and threshold overrides.

Untyped on purpose: the weights map is keyed by check name and the engine gains checks over time, so a struct here would silently drop any weight it did not know about and write it back as absent.

func (*Client) DeleteListEntry added in v0.2.0

func (c *Client) DeleteListEntry(ctx context.Context, id string) error

DeleteListEntry removes one rule by id.

func (*Client) DeviceObservations added in v0.2.0

func (c *Client) DeviceObservations(ctx context.Context, deviceID string) (map[string]any, error)

DeviceObservations returns the history behind one device id: what it has been seen doing and which subjects it links to.

This is the call that makes a device match interpretable. A raw fingerprint covers more than one real person on some platforms, so "this device matched" is only actionable once you can see WHAT it matched against.

func (*Client) Events

func (c *Client) Events(ctx context.Context, q EventsQuery) ([]EventRecord, error)

Events returns recent scored events, most recent first.

func (*Client) Forget

func (c *Client) Forget(ctx context.Context, in ForgetInput) (*ForgetResponse, error)

Forget erases everything held about a subject. For an erasure request under GDPR or CCPA, this is the call that satisfies it.

func (*Client) Health

func (c *Client) Health(ctx context.Context) (*Health, error)

Health reports API status and intel list sizes. Useful as a readiness probe for your own deploys.

func (*Client) IPInfo added in v0.2.0

func (c *Client) IPInfo(ctx context.Context) (map[string]any, error)

IPInfo returns intel for the caller's own IP, the same data the free tool shows. Useful as a smoke test that a key works.

func (*Client) Label

func (c *Client) Label(ctx context.Context, in LabelInput) (*LabelResponse, error)

Label records the ground truth for an event you scored earlier.

func (*Client) Lists added in v0.2.0

func (c *Client) Lists(ctx context.Context) (map[string]any, error)

Lists returns every allowlist and blocklist entry for the tenant.

func (*Client) Lookups added in v0.2.0

func (c *Client) Lookups(ctx context.Context) (map[string]any, error)

Lookups returns the tenant's lookup usage.

func (*Client) Score

func (c *Client) Score(ctx context.Context, e Event) (*ScoreResponse, error)

Score judges one event and returns a verdict with the evidence behind it.

On error, fail OPEN. An outage at a fraud vendor must never become an outage in your product, so the correct handling of a non-nil error is to allow the action and flag it for later review, not to refuse the user. The client already retries transient failures, so an error reaching you means the service is genuinely unavailable.

Example
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_1024",
		Email:    "new@example.com",
		IP:       "203.0.113.9",
		DeviceID: "b3f1c9…",
	})
	if err != nil {
		// Fail open: an outage at a fraud vendor must never become an outage
		// in your product.
		log.Printf("kaidn unavailable, allowing: %v", err)
		return
	}

	switch res.Verdict {
	case kaidn.VerdictBlock:
		fmt.Println("refused:", res.ReasonText)
	case kaidn.VerdictReview:
		fmt.Println("created, trial held:", res.Score)
	default:
		fmt.Println("created")
	}
}
Example (Checks)

Reading the checks is how you answer "why was this blocked" without guessing.

package main

import (
	"context"
	"fmt"
	"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: "cashout",
		Email: "payee@example.com",
	})
	if err != nil {
		return
	}
	for _, c := range res.Checks {
		fmt.Printf("%-24s %3d  %s\n", c.Reason, c.Weight, c.Message)
	}
}
Example (CollisionRisk)

A device match on a high-collision identity is a flag, not a verdict.

package main

import (
	"context"
	"fmt"
	"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:    "trial_start",
		DeviceID: "b3f1c9…",
	})
	if err != nil {
		return
	}
	if d := res.Device; d != nil && d.Resolution == "probabilistic" && d.CollisionRisk > 0.15 {
		fmt.Println("device match, but the identity is shared: review rather than block")
		return
	}
	fmt.Println(res.Verdict)
}

func (*Client) SetConfig added in v0.2.0

func (c *Client) SetConfig(ctx context.Context, cfg map[string]any) (map[string]any, error)

SetConfig replaces the tenant's overrides. Read Config first and modify what it returns, rather than constructing one from scratch, or you will clear overrides you did not mean to touch.

func (*Client) Stats

func (c *Client) Stats(ctx context.Context, windowHours int) (*Stats, error)

Stats summarises verdicts and reason codes over a rolling window.

func (*Client) Subject added in v0.2.0

func (c *Client) Subject(ctx context.Context, q SubjectQuery) (map[string]any, error)

Subject resolves everything held about one person across their identifiers.

func (*Client) Suppressions added in v0.2.0

func (c *Client) Suppressions(ctx context.Context, limit int) (map[string]any, error)

Suppressions returns subjects suppressed by an erasure request. Worth reading before a re-import: re-adding someone who asked to be forgotten is the kind of mistake that turns a process problem into a regulatory one.

type Device

type Device struct {
	ID         string `json:"id,omitempty"`
	ResolvedID string `json:"resolved_id,omitempty"`
	// "deterministic" (a token you issued) or "probabilistic" (a fingerprint).
	Resolution string `json:"resolution,omitempty"`
	// The measured chance this identity covers more than one real person.
	// Branch on it before acting on a device match alone.
	CollisionRisk float64 `json:"collision_risk,omitempty"`
}

Device describes the identity the engine resolved for this event, and how much to trust it.

type DeviceSignals

type DeviceSignals struct {
	// ── browser ────────────────────────────────────────────────────────────
	IsHeadless         *bool `json:"is_headless,omitempty"`
	UAConsistent       *bool `json:"ua_consistent,omitempty"`
	IsEmulated         *bool `json:"is_emulated,omitempty"`
	IsNoiseInjected    *bool `json:"is_noise_injected,omitempty"`
	IsTampered         *bool `json:"is_tampered,omitempty"`
	IsContextMismatch  *bool `json:"is_context_mismatch,omitempty"`
	IsEngineMismatch   *bool `json:"is_engine_mismatch,omitempty"`
	IsOSMismatch       *bool `json:"is_os_mismatch,omitempty"`
	IsFontStandardized *bool `json:"is_font_standardized,omitempty"`

	// AntidetectScore is 0-100 and AntidetectConfidence is "low", "medium" or
	// "high". Read the confidence before acting on the score: a competently
	// configured anti-detect browser that does not lie about its operating
	// system is not reliably detectable, by us or by anyone else.
	AntidetectScore      *int   `json:"antidetect_score,omitempty"`
	AntidetectConfidence string `json:"antidetect_confidence,omitempty"`

	// ── native mobile ──────────────────────────────────────────────────────
	IsEmulator     *bool `json:"is_emulator,omitempty"`
	IsRooted       *bool `json:"is_rooted,omitempty"`
	IsCloned       *bool `json:"is_cloned,omitempty"`
	IsHooked       *bool `json:"is_hooked,omitempty"`
	IsSideloaded   *bool `json:"is_sideloaded,omitempty"`
	IsADBEnabled   *bool `json:"is_adb_enabled,omitempty"`
	IsMockLocation *bool `json:"is_mock_location,omitempty"`
	IsVPNActive    *bool `json:"is_vpn_active,omitempty"`
	IsDebuggable   *bool `json:"is_debuggable,omitempty"`
}

DeviceSignals are the integrity flags the browser collector reports. All are optional; omitted fields are simply not scored. Pointers rather than plain bools throughout, because false and "not collected" are different facts. A plain bool would send is_rooted:false for every web visitor, asserting something the browser never checked.

type EmailCheck added in v0.2.0

type EmailCheck struct {
	FraudScore     float64 `json:"fraud_score"`
	IsDisposable   bool    `json:"is_disposable"`
	MXValid        bool    `json:"mx_valid"`
	CatchAll       bool    `json:"catch_all"`
	LooksGibberish bool    `json:"looks_gibberish"`
	HasPlusTag     bool    `json:"has_plus_tag"`
	// Canonical is the address with dots and plus-tags stripped. Two accounts
	// with different addresses and the same canonical are one inbox.
	Canonical    string   `json:"canonical"`
	IsAliased    bool     `json:"is_aliased"`
	AliasTricks  []string `json:"alias_tricks"`
	IsMalformed  bool     `json:"is_malformed"`
	RejectReason string   `json:"reject_reason"`
	RecentAbuse  bool     `json:"recent_abuse"`
}

EmailCheck is the intel held about one address.

type EmailCheckResponse added in v0.2.0

type EmailCheckResponse struct {
	Email      EmailCheck `json:"email"`
	Reputation Reputation `json:"reputation"`
	Summary    string     `json:"summary"`
}

type Error

type Error struct {
	// HTTP status, or 0 when the request never got a response.
	Status int
	// The API's error message when present, otherwise a description of the
	// transport failure.
	Message string
	// Raw response body, kept for the cases where the message is not enough.
	Body []byte
}

Error is returned for any non-2xx response, and for a network or timeout failure with Status 0.

Message is the API's own `{"error": …}` string when the response had one, rather than a generic "request failed", because the API's message is consistently the more useful of the two.

Example
package main

import (
	"context"
	"errors"
	"fmt"

	kaidn "github.com/kaidn-io/kaidn-go"
)

func main() {
	client := kaidn.New(kaidn.Options{APIKey: "wrong"})

	_, err := client.Score(context.Background(), kaidn.Event{Event: "signup"})

	var kerr *kaidn.Error
	if errors.As(err, &kerr) {
		switch {
		case kerr.QuotaExceeded():
			fmt.Println("out of events for this period")
		case kerr.Retryable():
			fmt.Println("transient, try again later")
		default:
			fmt.Println("permanent:", kerr.Message)
		}
	}
}

func (*Error) Error

func (e *Error) Error() string

func (*Error) QuotaExceeded

func (e *Error) QuotaExceeded() bool

QuotaExceeded reports whether the tenant is out of events for the period. Worth separating from other failures because the response is a product decision (prompt an upgrade, degrade gracefully) rather than a technical one.

func (*Error) Retryable

func (e *Error) Retryable() bool

Retryable reports whether the failure is transient and worth trying again: a network error, a rate limit, or a server fault.

A 4xx other than 429 is not retryable, and retrying it wastes quota and latency. Bad input and a bad API key do not become correct on the second attempt. The client uses this internally, and it is exported so callers building their own queue or backoff can make the same distinction.

type Event

type Event struct {
	// What happened, in your own vocabulary: "signup", "trial_start",
	// "cashout", "credit_redeem". Billing counts events, not fields.
	Event string `json:"event"`

	UserID string `json:"user_id,omitempty"`
	Email  string `json:"email,omitempty"`
	Phone  string `json:"phone,omitempty"`

	// The end user's IP, not your server's. Behind a proxy this must come from
	// a header your own infrastructure sets; see ClientIP in the README.
	IP string `json:"ip,omitempty"`

	// DeviceID is the probabilistic identifier from the browser collector.
	// Treat it as evidence, never as proof: identical settings produce
	// identical hashes, so unrelated people on default devices can share one.
	DeviceID string `json:"device_id,omitempty"`

	// DeviceToken is the deterministic first-party identifier you issued and
	// stored in your own cookie. When you have it, it beats DeviceID outright.
	DeviceToken string `json:"device_token,omitempty"`

	// Device carries browser integrity signals collected client-side.
	Device *DeviceSignals `json:"device,omitempty"`

	// Timezone as reported by the browser, e.g. "Europe/London". Compared
	// against the IP's location: a mismatch is a weak signal on its own and a
	// useful corroborator next to a proxy tell.
	Timezone string `json:"timezone,omitempty"`

	// PhoneCountry is the phone's country as an ISO-3166 alpha-2 code.
	PhoneCountry string `json:"phone_country,omitempty"`
	// EventCountry is the country the user claims (a form field, a profile).
	EventCountry string `json:"event_country,omitempty"`
	// IPCountry overrides the country the engine would derive from the IP.
	// Send it when you already have a geolocation you trust more than ours.
	IPCountry string `json:"ip_country,omitempty"`

	// ── attribution ────────────────────────────────────────────────────────
	// Where the user came from. These carry the affiliate-fraud signals: the
	// same identity converting under three different affiliates in a week is
	// not three customers, and no single partner's data can see that. Omitting
	// them does not break scoring, it just leaves that whole class of abuse
	// invisible.
	Source    string `json:"source,omitempty"`
	Site      string `json:"site,omitempty"`
	Campaign  string `json:"campaign,omitempty"`
	Affiliate string `json:"affiliate,omitempty"`
	Link      string `json:"link,omitempty"`
}

Event is one user action to score. Only Event is required.

Every other field is a signal, and each one you send is another check that can fire. Sending only an email still returns a verdict, it just has less to go on. There is no penalty for omitting a field you do not have, so send what you have rather than inventing placeholders: a fabricated IP is worse than no IP, because it will be scored as though it were real.

type EventRecord

type EventRecord struct {
	EventID   string   `json:"event_id"`
	Event     string   `json:"event"`
	UserID    string   `json:"user_id,omitempty"`
	Score     int      `json:"score"`
	Verdict   string   `json:"verdict"`
	Reasons   []string `json:"reasons"`
	CreatedAt string   `json:"created_at"`
}

type EventsQuery

type EventsQuery struct {
	Limit   int
	Verdict string
	Event   string
	UserID  string
}

EventsQuery filters the event history.

type ForgetInput

type ForgetInput struct {
	UserID   string `json:"user_id,omitempty"`
	Email    string `json:"email,omitempty"`
	DeviceID string `json:"device_id,omitempty"`
}

ForgetInput erases everything held about a subject, for a GDPR or CCPA erasure request.

type ForgetResponse

type ForgetResponse struct {
	Deleted int `json:"deleted"`
}

type Health

type Health struct {
	Status    string         `json:"status"`
	Intel     map[string]int `json:"intel,omitempty"`
	Narration string         `json:"narration,omitempty"`
}

type IPCheckResponse added in v0.2.0

type IPCheckResponse struct {
	IP         map[string]any `json:"ip"`
	Reputation Reputation     `json:"reputation"`
	Summary    string         `json:"summary"`
}

type LabelInput

type LabelInput struct {
	EventID string `json:"event_id"`
	// "fraud" or "legit".
	Label string `json:"label"`
	Note  string `json:"note,omitempty"`
}

LabelInput reports the ground truth for an event you scored earlier. Feeding outcomes back is what lets weights be tuned against reality rather than against intuition.

type LabelResponse

type LabelResponse struct {
	OK bool `json:"ok"`
}

type ListEntry added in v0.2.0

type ListEntry struct {
	ID    string `json:"id,omitempty"`
	List  string `json:"list"`
	Type  string `json:"type"`
	Value string `json:"value"`
}

ListEntry is one allowlist or blocklist rule. List is "allow" or "block", Type is the subject kind ("email", "ip", "device_id", "domain", "asn").

Allowlist entries short-circuit scoring entirely, which is what makes them the right tool for your own QA accounts and the wrong tool for anything you are not certain about.

type Options

type Options struct {
	// APIKey is your SECRET key. Never ship this to a browser: it can score,
	// read your event history and erase data. The browser wants the
	// publishable key instead, which can only submit fingerprints.
	APIKey string

	// BaseURL defaults to https://api.kaidn.io. Override it to point at a
	// local API in tests.
	BaseURL string

	// Timeout is the per-attempt timeout. Defaults to 5 seconds.
	//
	// Kept deliberately short because scoring sits in the critical path of a
	// signup or a payout. A fraud check must never be the reason a real
	// customer cannot register.
	Timeout time.Duration

	// Retries is the number of RETRIES after the first attempt, so 2 means at
	// most 3 requests. Defaults to 2. Only transient failures are retried:
	// see Error.Retryable.
	Retries int

	// HTTPClient injects your own client for tests, proxies or custom
	// transports. When set, Timeout is ignored and yours is authoritative.
	HTTPClient *http.Client
}

Options configures a Client. Only APIKey is required.

type PhoneCheckResponse added in v0.2.0

type PhoneCheckResponse struct {
	Phone      map[string]any `json:"phone"`
	Reputation Reputation     `json:"reputation"`
	Summary    string         `json:"summary"`
}

type Reputation added in v0.2.0

type Reputation struct {
	RecentAbuse bool    `json:"recent_abuse"`
	NetworkRisk float64 `json:"network_risk"`
	// How many distinct operators have seen this subject. The whole argument
	// for a shared graph is in this number.
	NetworkOperators int `json:"network_operators"`
	HoneypotHits     int `json:"honeypot_hits"`
}

Reputation is what the network has seen about a subject, as opposed to what the subject looks like on its own.

type ScoreResponse

type ScoreResponse struct {
	EventID string `json:"event_id"`
	// 0-100. Not a probability and not a confidence: it is the sum of the
	// weights that fired, clamped. The useful information is in Checks.
	Score int `json:"score"`
	// One of VerdictAllow, VerdictReview, VerdictBlock.
	Verdict string `json:"verdict"`
	// Reason codes, stable identifiers safe to branch on.
	Reasons []string `json:"reasons"`
	// The same reasoning as a sentence, for a review queue or a log line.
	ReasonText string  `json:"reason_text"`
	Checks     []Check `json:"checks"`
	Device     *Device `json:"device,omitempty"`
}

ScoreResponse is the result of scoring one event.

func (*ScoreResponse) Allowed

func (r *ScoreResponse) Allowed() bool

Allowed, NeedsReview and Blocked read better at a call site than comparing strings, and they keep the verdict values in one place.

func (*ScoreResponse) Blocked

func (r *ScoreResponse) Blocked() bool

func (*ScoreResponse) NeedsReview

func (r *ScoreResponse) NeedsReview() bool

type Stats

type Stats struct {
	Total    int            `json:"total"`
	Verdicts map[string]int `json:"verdicts"`
	Reasons  map[string]int `json:"reasons"`
}

type SubjectQuery added in v0.2.0

type SubjectQuery struct {
	Email    string `json:"email,omitempty"`
	Phone    string `json:"phone,omitempty"`
	IP       string `json:"ip,omitempty"`
	DeviceID string `json:"device_id,omitempty"`
	UserID   string `json:"user_id,omitempty"`
	Limit    int    `json:"limit,omitempty"`
}

SubjectQuery asks what is known about a subject across identifiers. Any combination may be given; Limit caps the events returned.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL