@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
- Go to Settings > Webhooks in your Offcoin admin dashboard
- Click Create webhook and enter your endpoint URL
- Select the events you want to receive
- 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:
- Concatenate timestamp and payload:
{timestamp}.{payload} - Compute HMAC-SHA256 using your webhook secret
- 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