npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tiaude

v0.0.1

Published

A TypeScript SDK that turns product events into explainable churn risk scores.

Readme

Tiaude

A TypeScript SDK that turns product events into explainable churn risk scores.

It is deterministic, configurable and stateless. It is not a machine learning model, does not store events, does not call a backend and does not collect data.

Install

npm install tiaude
pnpm add tiaude

Quick start

import { createChurnScorer } from "tiaude";

const scorer = createChurnScorer({
    baseRisk: 20,
    levels: {
        medium: 30,
        high: 60,
        critical: 80,
    },
    confidence: {
        targetRelevantEvents: 10,
    },
    freshness: {
        freshAfterHours: 24,
        staleAfterHours: 72,
    },
    signals: [
        {
            id: "inactive",
            type: "absence",
            event: "app.opened",
            weight: 30,
            halfLifeDays: 7,
            reason: "No recent activity",
            action: "Send a re-engagement message",
        },
        {
            id: "low_usage",
            type: "frequency_below",
            event: "feature.used",
            weight: 25,
            halfLifeDays: 7,
            threshold: 3,
            mode: "linear",
            reason: "Low recent product usage",
            action: "Suggest a relevant feature or workflow",
        },
        {
            id: "billing_failed",
            type: "occurrence",
            event: "billing.payment_failed",
            weight: 40,
            halfLifeDays: 3,
            reason: "Recent billing failure",
            action: "Ask the user to update their payment method",
        },
    ],
});

const result = scorer.track({
    userId: "user_123",
    previousState: user.churnState,
    event: {
        name: "feature.used",
        timestamp: new Date(),
    },
});

await db.user.update({
    where: { id: "user_123" },
    data: {
        churnState: result.state,
        churnRiskScore: result.riskScore,
        churnRiskLevel: result.riskLevel,
    },
});

Save result.state and pass it back on the next call. The SDK does not persist anything itself.

API

createChurnScorer(config)

Creates a scorer from your configuration.

const scorer = createChurnScorer(config);

The scorer exposes three methods:

scorer.track(input);
scorer.scoreUser(input);
scorer.refreshScore(input);

track()

Use track() when a new product event arrives.

const result = scorer.track({
    userId,
    previousState,
    event: {
        name: "feature.used",
        timestamp: new Date(),
    },
});

track() incrementally updates the previous state and returns a new score.

Rules:

  • previousState can be null or undefined for the first event.
  • Events must be sent in chronological order.
  • If now is provided, it must be greater than or equal to event.timestamp.
  • If the previous state belongs to another user, the SDK rejects it.
  • If the previous state was produced with an incompatible config, the SDK rejects it.

scoreUser()

Use scoreUser() to rebuild a score from historical events.

const result = scorer.scoreUser({
    userId,
    events,
});

scoreUser() sorts events by timestamp and rebuilds the state from zero.

Use it for:

  • first-time bootstrap;
  • full recompute;
  • recovery after out-of-order events;
  • recovery after config changes;
  • debugging or tests.

If now is provided, it must be greater than or equal to the latest event timestamp.

refreshScore()

Use refreshScore() when time passes but no new event arrives.

const result = scorer.refreshScore({
    userId,
    previousState,
});

refreshScore() recalculates time-sensitive signals without consuming a new event.

It updates the score, but it does not make old data fresh. Freshness is based on the latest relevant event, not only on the refresh time.

Result

type ChurnResult = {
    userId?: string;
    riskScore: number;
    riskLevel: "low" | "medium" | "high" | "critical";
    rawScore: number;
    confidence: number;
    freshness: "fresh" | "aging" | "stale";
    reasons: ChurnReason[];
    recommendedActions: string[];
    state: ChurnScoreState;
};

Typical fields to persist:

result.state;
result.riskScore;
result.riskLevel;

Typical fields to show in your product:

result.riskScore;
result.riskLevel;
result.reasons;
result.recommendedActions;
result.confidence;
result.freshness;

Events

type ChurnEvent = {
    name: string;
    timestamp: string | Date;
    properties?: Record<string, unknown>;
};

Event names are matched against your configured signals.

Timestamps can be passed as Date objects or supported ISO strings.

Recommended:

"2026-05-06T12:00:00.000Z";

Ambiguous or impossible dates are rejected:

"05/06/2026"; // invalid
"2026-02-31T00:00:00.000Z"; // invalid

Store your raw events

track() and refreshScore() are incremental optimizations. They let your app avoid loading and recomputing a user's full event history on every request.

They are not a replacement for storing raw events.

For production usage, your application should store product events in your own database. This allows you to run a full recompute with scoreUser() when needed.

You should use scoreUser() when:

  • the config changed in an incompatible way;
  • a signal was added, removed or changed;
  • events were received out of order;
  • historical events were edited or deleted;
  • a persisted state is missing, invalid or corrupted;
  • you are migrating SDK versions;
  • you want to debug or audit a score.

Recommended production pattern:

try {
    const result = scorer.track({
        userId,
        previousState: user.churnState,
        event,
    });

    await saveChurnState(userId, result.state);
} catch (error) {
    if (
        error instanceof ConfigMismatchError ||
        error instanceof OutOfOrderEventError ||
        error instanceof StateValidationError
    ) {
        const events = await loadUserEvents(userId);

        const result = scorer.scoreUser({
            userId,
            events,
        });

        await saveChurnState(userId, result.state);
        return;
    }

    throw error;
}

Config

type ChurnScorerConfig = {
    baseRisk: number;
    levels?: {
        medium: number;
        high: number;
        critical: number;
    };
    confidence?: {
        targetRelevantEvents: number;
    };
    freshness?: {
        freshAfterHours: number;
        staleAfterHours: number;
    };
    signals: ChurnSignal[];
};
type ChurnSignal = {
    id: string;
    type: "occurrence" | "absence" | "frequency_above" | "frequency_below";
    event: string;
    weight: number;
    halfLifeDays: number;
    threshold?: number;
    mode?: "linear" | "binary";
    reason: string;
    action?: string;
};

Signal types:

  • occurrence: risk based on a recent event;
  • absence: risk based on a missing or old event;
  • frequency_above: intensity increases when repeated usage is above a threshold;
  • frequency_below: intensity increases when repeated usage is below a threshold.

How scoring works

Each signal produces an intensity between 0 and 1.

impact = weight * intensity
rawScore = baseRisk + sum(impacts)
riskScore = clamp(rawScore, 0, 100)

A positive weight increases risk. A negative weight reduces risk.

The SDK also returns:

  • confidence: how much relevant signal is available;
  • freshness: how recent the latest relevant event is;
  • reasons: the strongest active signals;
  • recommendedActions: suggested actions from positive-risk signals.

State compatibility

Each returned state contains a stateCompatibilityHash.

If the config changes in a way that makes the previous state unsafe to reuse, the SDK throws ConfigMismatchError.

When this happens, rebuild the state with scoreUser() from historical events.

Changing only reason or action does not break state compatibility.

Error handling

import {
    ConfigMismatchError,
    OutOfOrderEventError,
    StateUserMismatchError,
} from "tiaude";

try {
    const result = scorer.track({
        userId,
        previousState,
        event,
    });

    await saveChurnState(userId, result.state);
} catch (error) {
    if (
        error instanceof ConfigMismatchError ||
        error instanceof OutOfOrderEventError
    ) {
        const result = scorer.scoreUser({
            userId,
            events: await loadUserEvents(userId),
        });

        await saveChurnState(userId, result.state);
        return;
    }

    if (error instanceof StateUserMismatchError) {
        // The previous state belongs to another user.
        // Do not reuse it.
        throw error;
    }

    throw error;
}

Exported errors:

  • ConfigValidationError
  • InvalidEventError
  • ConfigMismatchError
  • OutOfOrderEventError
  • StateValidationError
  • StateUserMismatchError

Notes

  • The SDK is framework-agnostic.
  • It works in Node.js, serverless functions and backend frameworks.
  • It has no runtime dependency.
  • It does not store raw events.
  • Your app is responsible for storing result.state.
  • Use scoreUser() when you need a full recompute.
  • Use refreshScore() for time-based recalculation without new events.

License

This project is licensed under the MIT License - see the LICENSE file for details.