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

@offcoin/sdk

v0.0.10

Published

TypeScript/JavaScript SDK for the Offcoin API

Readme

@offcoin/sdk

TypeScript/JavaScript SDK for the Offcoin API. Manage members, tokens, XP, achievements, and leaderboards.

Installation

npm install @offcoin/sdk
# or
pnpm add @offcoin/sdk
# or
yarn add @offcoin/sdk

Getting Started

Before using the SDK, you'll need to create a free or pro account on https://offcoin.space to get your clientId and clientSecret. These credentials are available in your account dashboard under Settings > API Access.

Quick Start

import { OffcoinClient } from '@offcoin/sdk';

const offcoin = new OffcoinClient({
  clientId: 'your_client_id',
  clientSecret: 'your_client_secret'
});

// List all members
const members = await offcoin.members.list();

// List members with metadata filter
const filteredMembers = await offcoin.members.list({
  metadata: { department: 'engineering', role: 'senior' }
});

// Create a member
const newMember = await offcoin.members.create({
  name: 'John Doe',
  aliases: ['discord:123456', 'github:johndoe'],
  metadata: { department: 'engineering', role: 'developer' } // optional
});

// Add tokens
const result = await offcoin.members.addTokens('discord:123456', 100, 'Welcome bonus');
console.log(`New balance: ${result.newBalance}`);

API Reference

Members

// List all members
const members = await offcoin.members.list();

// List members filtered by metadata
const filteredMembers = await offcoin.members.list({
  metadata: { department: 'engineering', role: 'senior' }
});

// Create a member
const member = await offcoin.members.create({
  name: 'John Doe',
  avatarUrl: 'https://example.com/avatar.png', // optional
  aliases: ['discord:123456', 'github:johndoe'], // optional
  metadata: { department: 'engineering', role: 'developer' } // optional
});

// Get a member by alias
const member = await offcoin.members.get('discord:123456');
// member.metadata contains the member's metadata

// Update a member
const updated = await offcoin.members.update('discord:123456', {
  name: 'Jane Doe',
  avatarUrl: null, // remove avatar
  metadata: { department: 'engineering', role: 'senior' } // merge with existing metadata
});

// Get token balance
const balance = await offcoin.members.getBalance('discord:123456');
console.log(`Balance: ${balance.balance}`);

// Get XP and level
const xp = await offcoin.members.getXp('discord:123456');
console.log(`XP: ${xp.xp}, Level: ${xp.level}`);

// Add tokens
const result = await offcoin.members.addTokens('discord:123456', 100, 'Purchase reward');

// Subtract tokens
const result = await offcoin.members.subtractTokens('discord:123456', 50, 'Item purchase');

// Add XP
const result = await offcoin.members.addXp('discord:123456', 25);

Member Aliases

// List all aliases for a member
const aliases = await offcoin.members.listAliases('discord:123456');

// Add alias 'github:johndoe' to a member with alias 'discord:johndoe'
const addResult = await offcoin.members.addAlias('discord:johndoe', 'github:johndoe');

// Update alias 'discord:123456' to 'discord:789012' for a member with alias 'github:123456'
const updateResult = await offcoin.members.updateAlias('github:123456', 'discord:123456', 'discord:789012');

// Remove alias 'github:johndoe' from a member with alias 'discord:123456'
const removeResult = await offcoin.members.removeAlias('discord:123456', 'github:johndoe');

Achievements

// List all achievements
const achievements = await offcoin.achievements.list();

// Unlock an achievement for a member
const result = await offcoin.achievements.unlock('achievement-id', 'discord:123456');
console.log(`Unlocked: ${result.achievementName}`);
console.log(`Cascading achievements: ${result.cascadingAchievementsGranted}`);

Leaderboard

// Get leaderboard (default: sorted by XP, limit 50, all-time)
const leaderboard = await offcoin.leaderboard.get();

// Get top 10 by tokens (all-time)
const tokenLeaderboard = await offcoin.leaderboard.get({
  sort: 'tokens',
  limit: 10
});

// Get top 25 by level (all-time)
const levelLeaderboard = await offcoin.leaderboard.get({
  sort: 'level',
  limit: 25
});

// Get current week leaderboard (gains this week)
const weeklyLeaderboard = await offcoin.leaderboard.get({
  sort: 'xp',
  period: 'current-week',
  limit: 20
});

// Get current month leaderboard (gains this month)
const monthlyLeaderboard = await offcoin.leaderboard.get({
  sort: 'tokens',
  period: 'current-month',
  limit: 50
});

// Get current year leaderboard (gains this year)
const yearlyLeaderboard = await offcoin.leaderboard.get({
  sort: 'level',
  period: 'current-year',
  limit: 100
});

// Get leaderboard filtered by metadata
const engineeringLeaderboard = await offcoin.leaderboard.get({
  sort: 'xp',
  period: 'current-month',
  limit: 20,
  metadata: { department: 'engineering', role: 'senior' }
});

Period Options:

  • 'all-time' (default): Shows current balances/XP (all-time leaderboard)
  • 'current-week': Shows gains from the start of the current week (Monday 00:00 UTC)
  • 'current-month': Shows gains from the start of the current month
  • 'current-year': Shows gains from the start of the current year

Note: Time-based periods (current-week, current-month, current-year) show only members who have activity (gains) in that period. Members with zero activity are excluded.

Permissions

// List all purchasable permissions
const permissions = await offcoin.permissions.list();

// List permissions with ownership info for a specific member
const permissions = await offcoin.permissions.list({ alias: 'discord:123456' });
// Returns permissions with `owned` and `canPurchase` fields

// Purchase a permission for a member
const result = await offcoin.permissions.purchase('discord:123456', 'vip_access');
console.log(`Purchased ${result.permissionName} for ${result.tokenPrice} tokens`);
console.log(`New balance: ${result.newBalance}`);

Webhooks

Offcoin can send HTTP notifications to your server when events occur. The SDK provides utilities for verifying webhook signatures to ensure authenticity.

Setting Up Webhooks

  1. Go to Settings > Webhooks in your Offcoin admin dashboard
  2. Click Create webhook and enter your endpoint URL
  3. Select the events you want to receive
  4. Copy the signing secret (shown only once)

Event Types

| Event | Description | |-------|-------------| | member.balance_updated | Token balance changed (add/subtract) | | member.xp_updated | XP changed | | achievement.unlocked | Member unlocked an achievement | | permission.granted | Member received a permission (via level or purchase) |

Verifying Webhook Signatures

Always verify webhook signatures to ensure requests are from Offcoin:

import { verifyWebhookSignature, WebhookEventTypes } from '@offcoin/sdk';

// Express.js example - note: you need raw body, not parsed JSON
app.post('/webhooks/offcoin', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['x-webhook-signature'] as string;
  const timestamp = req.headers['x-webhook-timestamp'] as string;
  const rawBody = req.body.toString();

  const result = await verifyWebhookSignature(
    rawBody,
    signature,
    timestamp,
    process.env.OFFCOIN_WEBHOOK_SECRET!
  );

  if (!result.valid) {
    console.error('Webhook verification failed:', result.error);
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process the verified webhook
  const { type, data } = result.payload!;

  switch (type) {
    case WebhookEventTypes.MEMBER_BALANCE_UPDATED:
      console.log(`Balance: ${data.memberName} now has ${data.newBalance} tokens`);
      break;
    case WebhookEventTypes.MEMBER_XP_UPDATED:
      console.log(`XP: ${data.memberName} is now level ${data.newLevel}`);
      break;
    case WebhookEventTypes.ACHIEVEMENT_UNLOCKED:
      console.log(`Achievement: ${data.memberName} unlocked "${data.achievementName}"`);
      break;
    case WebhookEventTypes.PERMISSION_GRANTED:
      console.log(`Permission: ${data.memberName} received "${data.permissionName}"`);
      break;
  }

  res.status(200).json({ received: true });
});

SvelteKit Example

// src/routes/webhooks/offcoin/+server.ts
import { json } from '@sveltejs/kit';
import { verifyWebhookSignature } from '@offcoin/sdk';
import { OFFCOIN_WEBHOOK_SECRET } from '$env/static/private';

export async function POST({ request }) {
  const signature = request.headers.get('x-webhook-signature');
  const timestamp = request.headers.get('x-webhook-timestamp');
  const rawBody = await request.text();

  const result = await verifyWebhookSignature(
    rawBody,
    signature ?? '',
    timestamp ?? '',
    OFFCOIN_WEBHOOK_SECRET
  );

  if (!result.valid) {
    return json({ error: result.error }, { status: 401 });
  }

  const { type, data } = result.payload!;
  // Handle webhook...

  return json({ received: true });
}

Webhook Payload Structure

All webhooks include these fields:

interface WebhookPayload {
  id: string;        // Unique event ID
  type: string;      // Event type (e.g., "member.balance_updated")
  timestamp: string; // ISO 8601 timestamp
  tenantId: string;  // Your workspace ID
  data: object;      // Event-specific data
}

Webhook Types

import type {
  WebhookPayload,
  BalanceUpdatedData,
  XpUpdatedData,
  AchievementUnlockedData,
  PermissionGrantedData
} from '@offcoin/sdk';

// Type-safe webhook handling
function handleBalanceUpdate(payload: WebhookPayload<BalanceUpdatedData>) {
  const { memberId, memberName, previousBalance, newBalance, amount, reason } = payload.data;
}

Verification Options

const result = await verifyWebhookSignature(
  rawBody,
  signature,
  timestamp,
  secret,
  {
    maxAgeSeconds: 300 // Reject webhooks older than 5 minutes (default)
  }
);

Manual Signature Verification

If you prefer to implement verification yourself, here's the algorithm:

  1. Concatenate timestamp and payload: {timestamp}.{payload}
  2. Compute HMAC-SHA256 using your webhook secret
  3. Compare with signature header (format: v1={hex})
// Headers sent with each webhook
// X-Webhook-Signature: v1=abc123...
// X-Webhook-Timestamp: 1702828800
// X-Webhook-Id: evt_xxx

const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = 'v1=' + hmacSHA256(signedPayload, secret);

Error Handling

The SDK throws typed errors for different scenarios:

import {
  OffcoinClient,
  AuthenticationError,
  NotFoundError,
  ValidationError
} from '@offcoin/sdk';

try {
  await offcoin.members.get('unknown-alias');
} catch (error) {
  if (error instanceof AuthenticationError) {
    console.error('Invalid credentials');
  } else if (error instanceof NotFoundError) {
    console.error('Member not found');
  } else if (error instanceof ValidationError) {
    console.error('Validation failed:', error.message);
  }
}

Error Types

| Error Class | Code | Description | |------------|------|-------------| | AuthenticationError | UNAUTHORIZED | Invalid client_id or client_secret | | ForbiddenError | FORBIDDEN | Not authorized for this action | | NotFoundError | NOT_FOUND | Resource not found | | ValidationError | VALIDATION_ERROR | Invalid input data | | BadRequestError | BAD_REQUEST | Malformed request | | InternalError | INTERNAL_ERROR | Server error | | OffcoinError | Various | Base error class |

TypeScript Types

All types are exported for use in your application:

import type {
  Member,
  MemberBalance,
  MemberXp,
  Achievement,
  LeaderboardEntry,
  TokenOperationResult,
  XpOperationResult,
  UnlockResult,
  Permission,
  PurchasePermissionResult,
  ListMembersOptions,
  // Webhook types
  WebhookPayload,
  WebhookEventType,
  BalanceUpdatedData,
  XpUpdatedData,
  AchievementUnlockedData,
  PermissionGrantedData
} from '@offcoin/sdk';

Authentication

The SDK uses Basic Authentication with your API credentials. You can find your clientId and clientSecret in the Offcoin admin dashboard under Settings > API Access.

const offcoin = new OffcoinClient({
  clientId: process.env.OFFCOIN_CLIENT_ID!,
  clientSecret: process.env.OFFCOIN_CLIENT_SECRET!
});

Requirements

  • Node.js 18+ (for native fetch support) or a browser environment
  • TypeScript 5.0+ (for type definitions)

License

MIT