
Security News
npm v12 Ships With Install Scripts Off by Default, Begins Deprecating 2FA-Bypass Tokens
npm v12 is generally available, turning install scripts off by default and beginning the deprecation of 2FA-bypass publishing tokens.
@offcoin/sdk
Advanced tools
TypeScript/JavaScript SDK for the Offcoin API. Manage members, tokens, XP, achievements, and leaderboards.
npm install @offcoin/sdk
# or
pnpm add @offcoin/sdk
# or
yarn add @offcoin/sdk
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.
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}`);
// 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);
// 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');
// 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}`);
// 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 yearNote: 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.
// 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}`);
Offcoin can send HTTP notifications to your server when events occur. The SDK provides utilities for verifying webhook signatures to ensure authenticity.
| 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) |
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 });
});
// 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 });
}
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
}
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;
}
const result = await verifyWebhookSignature(
rawBody,
signature,
timestamp,
secret,
{
maxAgeSeconds: 300 // Reject webhooks older than 5 minutes (default)
}
);
If you prefer to implement verification yourself, here's the algorithm:
{timestamp}.{payload}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);
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 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 |
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';
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!
});
MIT
FAQs
TypeScript/JavaScript SDK for the Offcoin API
We found that @offcoin/sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
npm v12 is generally available, turning install scripts off by default and beginning the deprecation of 2FA-bypass publishing tokens.

Research
/Security News
Socket tracks the activity as Operation “Muck and Load”: a threat actor uses commit-farming workflows, public dead drops, and protected archives to stage Windows RAT and infostealer malware.

Security News
pnpm 11.10 hardens registry auth to block token redirection, tightens pack-app and deploy, and makes the Rust port (v12) installable.