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

@hermes-core/api-client

v0.2.0

Published

API client for Hermes

Downloads

250

Readme

@hermes-core/api-client

Node.js SDK for the Hermes API — a multichain Discord tipbot platform supporting tips, airdrops, gifts, faucets, guild wallets, trivia, and real-time Socket.IO events.

Installation

npm install @hermes-core/api-client

Quick Start

import { createHermesClient } from '@hermes-core/api-client';

const client = createHermesClient({
  apiUrl: 'https://your-hermes-api.com',
  socketUrl: 'https://your-hermes-api.com',
  apiKey: 'YOUR_API_KEY',
});

// Public endpoints (no auth required)
const chains = await client.public.getChains();
const stats = await client.public.getStats();

// Private endpoints (requires API key)
const wallets = await client.private.getWallets();
const user = await client.private.getUser();

// Real-time events
client.socket.socket.on('wallets_updated', (data) => {
  console.log('Wallets changed:', data);
});

Configuration

const client = createHermesClient({
  apiUrl: 'https://your-hermes-api.com',   // API base URL (default: process.env.API_URL)
  socketUrl: 'https://your-hermes-api.com', // Socket.IO URL (default: process.env.SOCKET_URL)
  apiKey: 'YOUR_API_KEY',                   // API key (default: process.env.API_KEY)
  guildSecurity: {
    autoRefresh: true,                      // Auto-refresh guild sessions before expiry (default: true)
    defaultTtlSeconds: 1800,                // Default session TTL (optional)
    sessions: {},                           // Pre-loaded sessions (optional)
    onSessionUpdate: (guildId, session) => { // Callback on session changes (optional)
      // Persist session to disk/DB for recovery
    },
  },
});

The client exposes three namespaces:

  • client.public — Unauthenticated endpoints (chain info, guild listings, trivia, stats)
  • client.private — Authenticated endpoints (wallets, tips, airdrops, gifts, guild management)
  • client.socket — Real-time Socket.IO connection

Public API

These endpoints require no authentication.

Chains

// List all active chains
const chains = await client.public.getChains();

// Get chain details by name
const chain = await client.public.getChainByName('runebase');

Coins

const coins = await client.public.getCoins();

Guilds

// List guilds (public directory)
const guilds = await client.public.getGuilds({
  orderBy: 'memberCount',  // optional: 'memberCount', 'createdAt', etc.
  orderDir: 'DESC',        // optional: 'ASC' or 'DESC'
  limit: 25,               // optional
  offset: 0,               // optional
  search: 'crypto',        // optional: search by name
});

// Get guild details (channels, roles, emojis, stats, vote status)
const guild = await client.public.getGuildDetails('GUILD_ID');

// Get guild wallets (public balances)
const wallets = await client.public.getGuildWallets('GUILD_ID');

// Get guild reviews
const reviews = await client.public.getGuildReviews('GUILD_ID', { page: 1, pageSize: 25 });

// Get guild faucets
const faucets = await client.public.getGuildFaucets('GUILD_ID');

// Get guild members
const members = await client.public.getGuildMembers('GUILD_ID');

// Get guild operations
const operations = await client.public.getGuildOperations('GUILD_ID', { page: 1, limit: 10 });

// Get guild rewards (bump or vote)
const rewards = await client.public.getGuildRewards('GUILD_ID', 'bump');

// Get guild schedules
const schedules = await client.public.getGuildSchedules('GUILD_ID', {
  limit: 100,
  offset: 0,
  active: true,  // optional: filter by active status
});

Operations

// Get operation by ID (shareable link)
const operation = await client.public.getOperationById('OPERATION_ID');

Gifts

// Look up a gift by code (public info)
const gift = await client.public.getGiftByCode('GIFT_CODE');

Stats & Health

const stats = await client.public.getStats();   // { servers, users }
const health = await client.public.health();

Trivia

// Browse categories and questions
const categories = await client.public.getTriviaCategories({ page: 1, pageSize: 25 });
const questions = await client.public.getTriviaQuestions({ categoryId: 'UUID', page: 1, pageSize: 25 });
const question = await client.public.getTriviaQuestionById('QUESTION_ID');

// Browse curation tickets
const pending = await client.public.getTriviaTicketsPending({ page: 1, pageSize: 25, type: 'user_reports' });
const resolved = await client.public.getTriviaTicketsResolved({ page: 1, pageSize: 25 });
const ticket = await client.public.getTriviaTicketById('TICKET_ID');

Private API

All private endpoints require a valid API key.

User & Auth

// Get current user profile
const user = await client.private.getUser();
// Returns: { id, globalName, avatar, premiumExpiry, airdropScheduleSlots,
//            publicProfile, publicWallet, publicOperations, publicTransactions,
//            isAdmin, createdAt, updatedAt }

// Update privacy settings
await client.private.updateUser({
  publicProfile: true,
  publicWallet: false,
  publicOperations: true,
  publicTransactions: false,
});

// Get user's Discord guilds
const guilds = await client.private.getUserGuilds();

Wallets

// Get all wallet balances
const wallets = await client.private.getWallets();
// Returns: [{ ticker, available, locked, updatedAt }, ...]

// Get deposit address for a chain
const deposit = await client.private.getDepositAddress('runebase');
// Returns: { address, memo }

// Withdraw to external address
const result = await client.private.withdraw({
  ticker: 'RUNES',
  chainName: 'runebase',
  amount: '100.5',
  address: 'RXyz123abc...',
  memo: null,              // optional, required for some chains
  idempotencyKey: 'uuid',  // optional, auto-generated if omitted
});

Tips

const result = await client.private.tip({
  ticker: 'RUNES',
  recipientIds: ['user_id_1', 'user_id_2'],
  amountPerRecipient: '1.00',
  notifyChannelId: 'channel_id',  // optional
  idempotencyKey: 'uuid',         // optional
});

Airdrops

All airdrop methods accept an optional idempotencyKey parameter.

// Reactdrop - users react with emoji to claim
await client.private.reactdrop({
  ticker: 'RUNES',
  amount: '100',
  channelId: 'CHANNEL_ID',
  duration: 300000,           // ms (default: 5 min)
  emoji: '🎉',
  roleId: 'ROLE_ID',         // optional: restrict to role
  captcha: 'math',           // 'math', 'none', or 'trivia'
  maxRecipients: '2000',
});

// Partydrop - similar to reactdrop without emoji/captcha
await client.private.partydrop({
  ticker: 'RUNES', amount: '100', channelId: 'CHANNEL_ID',
  duration: 300000, roleId: 'ROLE_ID', maxRecipients: '2000',
});

// Flood - instant split among active users
await client.private.flood({
  ticker: 'RUNES', amount: '100', maxRecipients: 50,
  channelId: 'CHANNEL_ID', roleId: 'ROLE_ID',
});

// Rain - random distribution to active users
await client.private.rain({
  ticker: 'RUNES', amount: '100', maxRecipients: 50,
  channelId: 'CHANNEL_ID', roleId: 'ROLE_ID',
});

// Soak - even split among online users
await client.private.soak({
  ticker: 'RUNES', amount: '100', maxRecipients: 50,
  channelId: 'CHANNEL_ID', roleId: 'ROLE_ID',
});

// Wave - timed distribution
await client.private.wave({
  ticker: 'RUNES', amount: '100', maxRecipients: 50,
  channelId: 'CHANNEL_ID', roleId: 'ROLE_ID',
});

// Sleet - slow drip airdrop
await client.private.sleet({
  ticker: 'RUNES', amount: '100', channelId: 'CHANNEL_ID',
  roleId: 'ROLE_ID', maxRecipients: 400, duration: 900000,
});

// Trivia - trivia-based airdrop
await client.private.trivia({
  ticker: 'RUNES', amount: '100', channelId: 'CHANNEL_ID',
  duration: 300000, roleId: 'ROLE_ID',
  categoryId: 'UUID',   // optional
  questionId: 'UUID',   // optional
  maxRecipients: '2000',
});

Gifts

// List your gifts
const gifts = await client.private.getGifts({ page: 1, limit: 10, status: 'all' });
// status: 'all', 'generated', 'partial', 'claimed', 'cancelled', 'expired'

// Create a coin gift
const gift = await client.private.createGift({
  giftType: 'coin',
  coinTicker: 'RUNES',
  amountPerClaim: '10',
  maxClaims: 100,
  expiresInDays: 30,    // optional (1-365)
  message: 'Enjoy!',    // optional (max 500 chars)
});
// Returns: { code, operationId, totalAmount, maxClaims, expiresAt }

// Create a premium gift
const premiumGift = await client.private.createGift({
  giftType: 'premium',
  coinTicker: 'RUNES',
  itemId: 'STORE_ITEM_ID',
  maxClaims: 5,
  targetType: 'user',  // 'user' or 'guild'
});

// Claim a gift
const claim = await client.private.claimGift('GIFT_CODE', {
  guildId: 'GUILD_ID',  // required if targetType is 'guild'
});

// Cancel a gift (refunds remaining balance)
const cancel = await client.private.cancelGift('GIFT_CODE');

Store

const purchase = await client.private.purchaseItem({
  itemId: 'ITEM_ID',
  guildId: 'GUILD_ID',           // optional
  coinTickerToBoost: 'RUNES',    // optional
});

Bulk Payouts

// Create a bulk payout
const payout = await client.private.bulkPayout({
  memo: 'Monthly rewards',
  notifyChannelId: 'CHANNEL_ID',
  payouts: [
    { recipientId: 'user_1', ticker: 'RUNES', amount: '100' },
    { recipientId: 'user_2', ticker: 'RUNES', amount: '50' },
  ],
});

// List your bulk payouts
const payouts = await client.private.getBulkPayouts({ page: 1, limit: 10 });

// Get bulk payout details
const detail = await client.private.getBulkPayoutById('PAYOUT_ID');

Trivia Submission & Curation

// Submit a new question
await client.private.submitTriviaQuestion({
  categoryId: 'CATEGORY_UUID',
  question: 'What is the capital of France?',
  options: ['London', 'Paris', 'Berlin', 'Madrid'],
  correctAnswerIndex: 1,
  difficulty: 'easy',       // optional
  explanation: 'Paris...',  // optional
});

// Submit a new category
await client.private.submitTriviaCategory({
  name: 'Geography',
  description: 'Questions about world geography',  // optional
  emoji: '🌍',                                      // optional
});

// Report a question
await client.private.reportTriviaQuestion('QUESTION_ID', {
  type: 'edit',  // 'edit' or 'delete'
  reason: 'Incorrect answer',
  proposedContent: { question: 'Corrected question text' },  // optional
});

// Get your tickets
const tickets = await client.private.getTriviaTicketsUser({ page: 1, pageSize: 25, state: 'open' });

// Vote on a ticket (fact checkers only)
await client.private.voteTriviaTicket('TICKET_ID', { voteType: 'approve' });

// Reactivate a disabled question (fact checkers only)
await client.private.reactivateTriviaQuestion('QUESTION_ID');

// Get trashed questions
const trashed = await client.private.getTriviaTrashedQuestions();

// Check fact checker status
const status = await client.private.isFactChecker();

// Request curation tokens
await client.private.requestTriviaTokens();

// Get your token balance
const tokens = await client.private.getTriviaTokens();

Guild Operations

Non-Privileged (auth only)

// Donate to a guild wallet
await client.private.guildDonation('GUILD_ID', {
  ticker: 'RUNES',
  amount: '100',
});

// Get invite link for adding Hermes bot
const invite = await client.private.getGuildInviteHermes('GUILD_ID');

// Get your roles in a guild
const roles = await client.private.getUserGuildRoles('GUILD_ID');

// Claim a faucet
const claim = await client.private.claimGuildFaucet('GUILD_ID', 'FAUCET_ID');

// Get vote/bump history
const voteHistory = await client.private.getGuildVoteHistory('GUILD_ID');
const bumpHistory = await client.private.getGuildBumpHistory('GUILD_ID');

// Get a specific schedule
const schedule = await client.private.getGuildSchedule('GUILD_ID', 'SCHEDULE_ID');

Privileged (requires guild security session)

Privileged operations require an active guild security session. The SDK handles session injection via X-Guild-Authorization headers, auto-refresh before expiry, and automatic retry on 401.

// Login to guild security
await client.private.guildSecurityLogin('GUILD_ID', {
  password: 'YOUR_PASSWORD',
  ttlMinutes: 30,  // or ttlSeconds: 1800
});

// All subsequent guild privileged calls auto-inject the session token:

// Guild tip
await client.private.guildTip('GUILD_ID', {
  ticker: 'RUNES',
  recipientIds: ['user_id_1'],
  amountPerRecipient: '1.00',
  notifyChannelId: 'CHANNEL_ID',
});

// Guild airdrops (same parameters as user airdrops)
await client.private.guildFlood('GUILD_ID', { ticker: 'RUNES', amount: '100', maxRecipients: 50, channelId: 'CH_ID' });
await client.private.guildRain('GUILD_ID', { ... });
await client.private.guildSoak('GUILD_ID', { ... });
await client.private.guildWave('GUILD_ID', { ... });
await client.private.guildSleet('GUILD_ID', { ... });
await client.private.guildReactdrop('GUILD_ID', { ... });
await client.private.guildPartydrop('GUILD_ID', { ... });
await client.private.guildTrivia('GUILD_ID', { ... });

// Guild bulk payout
await client.private.guildBulkPayout('GUILD_ID', {
  memo: 'Weekly rewards',
  notifyChannelId: 'CHANNEL_ID',
  payouts: [{ recipientId: 'user_1', ticker: 'RUNES', amount: '50' }],
});
const payouts = await client.private.getGuildBulkPayouts('GUILD_ID', { page: 1, limit: 10 });
const payout = await client.private.getGuildBulkPayoutById('GUILD_ID', 'PAYOUT_ID');

// Logout
await client.private.guildSecurityLogout('GUILD_ID');

Guild Settings (Privileged)

// Update guild settings
await client.private.updateGuildSettings('GUILD_ID', {
  publicOperations: true,
});

// Set faucet notification channel
await client.private.updateGuildFaucetNotifyChannel('GUILD_ID', {
  channelId: 'CHANNEL_ID',
});

Guild Faucets (Privileged)

// Create a faucet
const faucet = await client.private.createGuildFaucet('GUILD_ID', {
  ticker: 'RUNES',
  title: 'Daily Faucet',
  type: 'static',           // 'static' or 'percentage'
  value: '10',              // amount (static) or percentage (0.00001-100)
  claimInterval: 86400,     // seconds between claims (60 - 2592000)
  roleId: 'ROLE_ID',        // optional: restrict to role
});

// Delete a faucet
await client.private.deleteGuildFaucet('GUILD_ID', 'FAUCET_ID');

Guild Rewards (Privileged)

// Create/set a bump or vote reward
await client.private.createGuildReward('GUILD_ID', 'bump', {
  ticker: 'RUNES',
  amountType: 'static',   // 'static' or 'percentage'
  amount: '1.0',
});

// Update a reward
await client.private.updateGuildReward('GUILD_ID', 'vote', 'RUNES', {
  amount: '2.0',
  status: 'active',  // 'active' or 'inactive'
});

Guild Schedules (Privileged)

// Create a scheduled airdrop
const schedule = await client.private.createGuildSchedule('GUILD_ID', {
  ticker: 'RUNES',
  operationType: 'rain',
  channelId: 'CHANNEL_ID',
  amount: '100',
  amountType: 'static',          // optional: 'static' or 'percentage'
  scheduleType: 'cron',          // 'cron' or 'once'
  cronExpression: '0 12 * * *',  // required for cron type
  runAt: null,                   // required for once type (ISO date)
  roleId: 'ROLE_ID',            // optional
  maxRecipients: 50,             // optional
  isPublic: true,                // optional: visible to non-privileged users
});

// Update a schedule
await client.private.updateGuildSchedule('GUILD_ID', 'SCHEDULE_ID', {
  amount: '200',
  active: false,
});

// Delete a schedule
await client.private.deleteGuildSchedule('GUILD_ID', 'SCHEDULE_ID');

Guild Security Management

Initial Setup

// Check if guild security is configured
const status = await client.private.getGuildSecurityStatus('GUILD_ID');
// Returns: { configured, operator, isAuthorized, authSource }

// Claim ownership (first-time setup, must be Discord guild admin)
await client.private.guildSecurityClaim('GUILD_ID', {
  email: '[email protected]',
  password: 'secure_password',
});

Session Management

// Login
const session = await client.private.guildSecurityLogin('GUILD_ID', {
  password: 'YOUR_PASSWORD',
  ttlMinutes: 30,
});
// Returns: { accessToken, refreshToken, accessExpiresAt, refreshExpiresAt, accessTtlSeconds }

// Manual refresh
const refreshed = await client.private.guildSecurityRefresh('GUILD_ID', {
  refreshToken: session.refreshToken,
  ttlMinutes: 30,
});

// Logout
await client.private.guildSecurityLogout('GUILD_ID');

// Auto-refresh helpers
client.private.guildSecurityStartAutoRefresh('GUILD_ID');
client.private.guildSecurityStopAutoRefresh('GUILD_ID');
client.private.guildSecurityWarmup('GUILD_ID');

Operator Management (Owner only)

// List operators
const { operators } = await client.private.getGuildSecurityOperators('GUILD_ID');

// Add an operator
await client.private.addGuildSecurityOperator('GUILD_ID', {
  userId: 'DISCORD_USER_ID',
  password: 'operator_password',
  role: 'operator',  // 'operator' or 'owner'
});

// Remove an operator
await client.private.removeGuildSecurityOperator('GUILD_ID', 'USER_ID');

Role-Based Access (Owner only)

// List whitelisted security roles
const roles = await client.private.getGuildSecurityRoles('GUILD_ID');

// Add a Discord role as authorized
await client.private.addGuildSecurityRole('GUILD_ID', { roleId: 'DISCORD_ROLE_ID' });

// Remove a whitelisted role
await client.private.removeGuildSecurityRole('GUILD_ID', 'DISCORD_ROLE_ID');

Password & Email

// Change password (requires active session)
await client.private.guildSecurityPassword('GUILD_ID', {
  currentPassword: 'old_password',
  newPassword: 'new_password',
});

// Update recovery email (owner only, sends verification)
await client.private.guildSecurityEmail('GUILD_ID', {
  email: '[email protected]',
});

// Verify email change
await client.private.guildSecurityEmailVerify('GUILD_ID', {
  token: 'VERIFICATION_TOKEN',
});

// Request password reset (sends email)
await client.private.guildSecurityResetRequest('GUILD_ID', {
  email: '[email protected]',
});

// Confirm password reset
await client.private.guildSecurityResetConfirm('GUILD_ID', {
  token: 'RESET_TOKEN',
  newPassword: 'new_password',
});

Socket.IO Events

The client establishes a real-time Socket.IO connection with automatic reconnection and heartbeat.

const { socket } = client.socket;

// Wallet balance changes
socket.on('wallets_updated', (wallets) => {
  console.log('Wallets updated:', wallets);
});

// Server heartbeat
socket.on('pong', () => {
  console.log('Server is alive');
});

The socket automatically:

  • Authenticates via API key on connect
  • Joins the private channel (joinPrivate)
  • Reconnects with exponential backoff on disconnection
  • Sends heartbeat pings every 30 seconds

Guild Security Auto-Refresh

The SDK manages guild security sessions with:

  • Automatic refresh before token expiry (configurable safety window + jitter)
  • Single-flight refresh to prevent concurrent refresh requests
  • Exponential backoff on refresh failures (10s, 20s, 40s... up to 5 min)
  • 401 retry on privileged requests: if a request returns 401, the SDK refreshes the session and retries once
  • Session persistence via onSessionUpdate callback for crash recovery
const client = createHermesClient({
  apiUrl: 'https://your-api.com',
  apiKey: 'YOUR_KEY',
  guildSecurity: {
    autoRefresh: true,
    defaultTtlSeconds: 1800,
    onSessionUpdate: (guildId, session) => {
      // Save to file/DB for persistence across restarts
      if (session) {
        saveSession(guildId, session);
      } else {
        deleteSession(guildId);  // null = session cleared (logout)
      }
    },
    // Pre-load saved sessions on startup
    sessions: {
      'GUILD_ID': loadSession('GUILD_ID'),
    },
  },
});

Idempotency

All financial operations (tips, airdrops, withdrawals, gifts, purchases, donations, bulk payouts) support idempotency via the X-Idempotency-Key header. If you omit the idempotencyKey parameter, a random UUID is generated automatically.

Pass your own key to safely retry operations without double-processing:

const myKey = 'payment-2024-01-15-user123';

await client.private.tip({
  ticker: 'RUNES',
  recipientIds: ['user_1'],
  amountPerRecipient: '10',
  idempotencyKey: myKey,
});

// Safe to retry with same key - won't double-tip
await client.private.tip({
  ticker: 'RUNES',
  recipientIds: ['user_1'],
  amountPerRecipient: '10',
  idempotencyKey: myKey,
});

Error Handling

All methods throw an Error with the server's error message when a request fails:

try {
  await client.private.withdraw({
    ticker: 'RUNES', chainName: 'runebase',
    amount: '999999', address: 'invalid',
  });
} catch (error) {
  console.error(error.message); // e.g. "Insufficient balance"
}

Integration Tests

# Run all tests
node src/test-integration.mjs

# Run specific tests (comma-separated flags)
node src/test-integration.mjs --wallets --tip --sockets

# Run a single test
node src/test-integration.mjs --guild-tip