@offcoin/sdk
TypeScript/JavaScript SDK for the Offcoin API. Manage members, tokens, XP, achievements, and leaderboards.
Installation
npm install @offcoin/sdk
pnpm add @offcoin/sdk
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'
});
const members = await offcoin.members.list();
const filteredMembers = await offcoin.members.list({
metadata: { department: 'engineering', role: 'senior' }
});
const newMember = await offcoin.members.create({
name: 'John Doe',
aliases: ['discord:123456', 'github:johndoe'],
metadata: { department: 'engineering', role: 'developer' }
});
const result = await offcoin.members.addTokens('discord:123456', 100, 'Welcome bonus');
console.log(`New balance: ${result.newBalance}`);
API Reference
Members
const members = await offcoin.members.list();
const filteredMembers = await offcoin.members.list({
metadata: { department: 'engineering', role: 'senior' }
});
const member = await offcoin.members.create({
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.png',
aliases: ['discord:123456', 'github:johndoe'],
metadata: { department: 'engineering', role: 'developer' }
});
const member = await offcoin.members.get('discord:123456');
const updated = await offcoin.members.update('discord:123456', {
name: 'Jane Doe',
avatarUrl: null,
metadata: { department: 'engineering', role: 'senior' }
});
const balance = await offcoin.members.getBalance('discord:123456');
console.log(`Balance: ${balance.balance}`);
const xp = await offcoin.members.getXp('discord:123456');
console.log(`XP: ${xp.xp}, Level: ${xp.level}`);
const result = await offcoin.members.addTokens('discord:123456', 100, 'Purchase reward');
const result = await offcoin.members.subtractTokens('discord:123456', 50, 'Item purchase');
const result = await offcoin.members.addXp('discord:123456', 25);
Achievements
const achievements = await offcoin.achievements.list();
const result = await offcoin.achievements.unlock('achievement-id', 'discord:123456');
console.log(`Unlocked: ${result.achievementName}`);
console.log(`Cascading achievements: ${result.cascadingAchievementsGranted}`);
Leaderboard
const leaderboard = await offcoin.leaderboard.get();
const tokenLeaderboard = await offcoin.leaderboard.get({
sort: 'tokens',
limit: 10
});
const levelLeaderboard = await offcoin.leaderboard.get({
sort: 'level',
limit: 25
});
const weeklyLeaderboard = await offcoin.leaderboard.get({
sort: 'xp',
period: 'current-week',
limit: 20
});
const monthlyLeaderboard = await offcoin.leaderboard.get({
sort: 'tokens',
period: 'current-month',
limit: 50
});
const yearlyLeaderboard = await offcoin.leaderboard.get({
sort: 'level',
period: 'current-year',
limit: 100
});
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
const permissions = await offcoin.permissions.list();
const permissions = await offcoin.permissions.list({ alias: 'discord:123456' });
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
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';
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' });
}
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
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!;
return json({ received: true });
}
Webhook Payload Structure
All webhooks include these fields:
interface WebhookPayload {
id: string;
type: string;
timestamp: string;
tenantId: string;
data: object;
}
Webhook Types
import type {
WebhookPayload,
BalanceUpdatedData,
XpUpdatedData,
AchievementUnlockedData,
PermissionGrantedData
} from '@offcoin/sdk';
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
}
);
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})
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
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,
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