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

@molecule/api-reputation

v1.0.1

Published

Per-user reputation/karma scoring + badge tracking.

Downloads

495

Readme

@molecule/api-reputation

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Generic reputation/karma engine for molecule.dev.

Per-user reputation scoring with append-only event history, idempotent badge awards, and a pure {@link computeLevel} helper for deriving levels from configurable thresholds. The service layer persists via the abstract @molecule/api-database DataStore — no raw SQL leaks into handler-callable code.

Pairs with the frontend display package @molecule/app-reputation-badge-react.

Quick Start

import { recordEvent, getScore, awardBadge } from '@molecule/api-reputation'

await recordEvent('user-1', 'accepted-solution', 15, {
  sourceId: 'comment-42',
})

const score = await getScore('user-1')
console.log(score.score, score.level)

if (score.score >= 1000) {
  await awardBadge('user-1', 'top-contributor')
}

Type

resource

Installation

npm install @molecule/api-reputation @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource

API

Interfaces

Badge

Idempotent badge award. A user has at most one row per (userId, kind) pair.

interface Badge {
  /** Badge identifier (UUID). */
  id: string
  /** The user the badge was awarded to. */
  userId: string
  /**
   * Badge kind (e.g. `first-post`, `top-1-percent`, `helpful-answer`).
   * Free-form; apps decide their own taxonomy.
   */
  kind: string
  /** Award timestamp. */
  awardedAt: Date
}

ReputationEvent

Append-only reputation event. The score table is the materialised sum of all events for a user; events are retained for audit and recompute scenarios.

interface ReputationEvent {
  /** Event identifier (UUID). */
  id: string
  /** The user whose reputation was affected. */
  userId: string
  /**
   * Domain-specific event kind (e.g. `vote`, `like`,
   * `accepted-solution`, `post`, `report-rejected`). Free-form string;
   * apps decide their own taxonomy.
   */
  kind: string
  /** Signed integer applied to the user's score. May be negative. */
  delta: number
  /**
   * Optional reference to the domain object that triggered the event
   * (e.g. a comment ID, post ID, vote ID). Used to deduplicate
   * recompute jobs and to power audit views.
   */
  sourceId?: string | null
  /**
   * Optional structured metadata (JSON). Persisted as-is by the
   * database bond; consumers should treat as opaque unless they
   * authored the event.
   */
  metadata?: Record<string, unknown> | null
  /** Event creation timestamp. */
  createdAt: Date
}

ReputationEventSource

Optional source descriptor for {@link ReputationEvent}s — accepted as a single argument to keep the service signature ergonomic.

interface ReputationEventSource {
  /** Optional domain-object reference. */
  sourceId?: string
  /** Optional structured metadata. */
  metadata?: Record<string, unknown>
}

ReputationScore

Persisted reputation snapshot for a single user.

interface ReputationScore {
  /** The user identifier (PK). */
  userId: string
  /** Cumulative score across all recorded events. */
  score: number
  /** Derived level — see {@link DEFAULT_LEVEL_THRESHOLDS}. */
  level: number
  /** Timestamp of the last score-bumping event. */
  updatedAt: Date
}

Functions

awardBadge(userId, badgeKind)

Awards a badge to a user. Idempotent: if a badge of the same kind already exists, the existing record is returned unchanged.

function awardBadge(userId: string, badgeKind: string): Promise<Badge>
  • userId — The user identifier.
  • badgeKind — Badge kind (e.g. first-post, top-1-percent).

Returns: The (possibly pre-existing) {@link Badge}.

computeLevel(score, thresholds)

Computes the level for a given score against ascending thresholds.

Threshold semantics: index i is the lower bound (inclusive) for level i. The returned level is the highest index whose threshold is <= score. Negative scores are clamped to level 0.

function computeLevel(score: number, thresholds?: readonly number[]): number
  • score — Cumulative reputation score.
  • thresholds — Ascending lower bounds per level. Defaults to {@link DEFAULT_LEVEL_THRESHOLDS}. Must be non-empty and ascending.

Returns: The derived level (always >= 0).

getBadges(req, res)

Returns the list of badges awarded to the user identified by the :id route param, newest first. Public — no authentication required.

function getBadges(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — Express-compatible request.
  • res — Express-compatible response.

getEvents(userId, limit)

Reads recent reputation events for a user, newest first.

function getEvents(userId: string, limit?: number): Promise<ReputationEvent[]>
  • userId — The user identifier.
  • limit — Maximum number of events to return (default 50).

Returns: An array of {@link ReputationEvent}s.

getReputation(req, res)

Returns the public reputation snapshot for the user identified by the :id route param. Public — no authentication required.

function getReputation(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — Express-compatible request.
  • res — Express-compatible response.

getScore(userId)

Reads the current score for a user. Returns a zeroed score when no row exists.

function getScore(userId: string): Promise<ReputationScore>
  • userId — The user identifier.

Returns: The current {@link ReputationScore}.

listBadges(userId)

Lists all badges awarded to a user, newest first.

function listBadges(userId: string): Promise<Badge[]>
  • userId — The user identifier.

Returns: An array of {@link Badge}s.

recordEvent(userId, kind, delta, source)

Records a reputation event and atomically bumps the user's score.

Appends a row to reputation_events and then upserts the user's row in reputation_scores. The new level is recomputed from the resulting score using {@link computeLevel}.

function recordEvent(
  userId: string,
  kind: string,
  delta: number,
  source?: ReputationEventSource,
): Promise<ReputationScore>
  • userId — The user identifier.
  • kind — Domain-specific event kind (e.g. vote, like).
  • delta — Signed integer applied to the user's score.
  • source — Optional source descriptor (sourceId, metadata).

Returns: The updated {@link ReputationScore}.

revokeBadge(userId, badgeKind)

Revokes a badge from a user. No-op when the badge is not present.

function revokeBadge(userId: string, badgeKind: string): Promise<boolean>
  • userId — The user identifier.
  • badgeKind — Badge kind to revoke.

Returns: true when a badge was removed, false otherwise.

Constants

DEFAULT_LEVEL_THRESHOLDS

Default level thresholds — index i is the lower bound (inclusive) for level i. Level 0 covers score < 100, level 1 covers 100 <= score < 500, and so on.

Ordered ascending. Apps may pass their own thresholds to {@link computeLevel} if a different curve is desired.

const DEFAULT_LEVEL_THRESHOLDS: readonly number[]

requestHandlerMap

Handler map for reputation routes (getReputation, getBadges).

const requestHandlerMap: {
  readonly getReputation: typeof getReputation
  readonly getBadges: typeof getBadges
}

routes

Routes for public reputation reads. No authentication is required: reputation/badge data is treated as public profile information by the social-app templates that consume this package.

const routes: readonly [
  {
    readonly method: 'get'
    readonly path: '/users/:id/reputation'
    readonly handler: 'getReputation'
    readonly middlewares: readonly []
  },
  {
    readonly method: 'get'
    readonly path: '/users/:id/badges'
    readonly handler: 'getBadges'
    readonly middlewares: readonly []
  },
]

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-database ^1.0.1
  • @molecule/api-i18n ^1.0.1
  • @molecule/api-logger ^1.0.1
  • @molecule/api-resource ^1.0.1

Runtime Dependencies

  • @molecule/api-database

  • @molecule/api-i18n

  • @molecule/api-logger

  • @molecule/api-resource

  • Migration required. Three files ship in src/__setup__/ (reputation_events.sql, reputation_scores.sql, badges.sql) and must exist in the target database before use (scaffolded apps apply them automatically; existing apps must apply them first).

  • Mutations are server-internal ONLY — the shipped routes are read-only. recordEvent(), awardBadge(), revokeBadge() are service functions meant to be called from YOUR domain code (an accepted-answer handler, a moderation hook, a cron job). NEVER expose them on a route that accepts kind / delta / badgeKind from the client — a client-supplied delta is score tampering. The server decides the delta for each domain event.

  • Reads are PUBLIC by design. GET /users/:id/reputation and GET /users/:id/badges ship with no auth middleware (public-profile data for social apps). If reputation is private in your app, add an authorizer.

  • awardBadge() is idempotent (re-awarding returns the existing row); recordEvent() is NOT — guard call sites against double-firing, and record a compensating negative event for undo (the event history is append-only).

  • computeLevel(score, thresholds) is pure and accepts custom thresholds, but the level stored by recordEvent() uses the DEFAULT thresholds — recompute client-side from your own thresholds if you customize them.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual reputation-earning flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • [ ] A reputation-earning action (an upvote, an accepted answer, whatever this app awards for) bumps the actor's total by exactly the points that action is worth: note the score shown in the UI before, perform the action, then confirm the new total equals old plus that delta — the arithmetic is exact, not just "the number went up".
  • [ ] Levels/tiers track the total at the right thresholds: as the score crosses a threshold the displayed level advances by one, and a score just below that threshold does NOT advance. Badges appear only once earned and stay a single copy — re-earning the same badge never adds a duplicate.
  • [ ] If this app surfaces a leaderboard or ranking, it orders users by their real totals — the top user has the highest score, and a change to one user's total re-sorts the list correctly.
  • [ ] Anti-gaming — points cannot be farmed: repeating the SAME source action (double-clicking one upvote, re-firing a single accepted answer) awards the points once, not per click; a user cannot award themselves (no self-upvote or self-award inflates their own total); and any daily or per-source cap the app defines actually stops further points once it is hit.
  • [ ] Reversing an action deducts what it granted: undo the upvote or delete the post that earned points and confirm the actor's total drops back by the same amount — an undo leaves the score honest, never stranded high.
  • [ ] Reputation is awarded by the server alone: there is NO request a user can send to set their own score, level, points, or badge directly — no form field or API parameter feeds the delta. A user sees everyone's public rep but the only thing that changes it is a real earning action the server scored.