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

@appmerge/appactor-sdk

v3.0.1

Published

Official Node.js SDK for AppActor - Manage apps and subscribers with ease

Downloads

792

Readme

AppActor Node.js SDK

Official Node.js SDK for AppActor - Manage apps, subscribers, and token-based subscriptions via server-side API.

Installation

npm install @appmerge/appactor-sdk

Quick Start

const AppActor = require('@appmerge/appactor-sdk');

const client = new AppActor('sk_your_project_secret_key');

// Get subscriber info
const user = await client.users.get('user-id');
console.log(user);

// Get project token balance
const tokens = await client.users.getTokenBalances('user-id');
console.log(tokens.tokenBalance.total);

// Debit tokens with project secret + user id. No appId needed.
await client.users.debitTokens('user-id', 5, {
  idempotencyKey: 'request-123',
  metadata: { feature: 'video-gen' },
});

// App-specific operations still use forApp(appId)
const appClient = client.forApp('your_app_id');
const app = await appClient.apps.get();
console.log(app.name, app.platform);

Features

  • App Info - Retrieve app details for a selected app
  • Users/Subscribers - Look up user info, entitlements, subscriptions, and token balances
  • Token Operations - Debit/grant tokens with smart renewable-first priority
  • Caching - Automatic 1-hour in-memory cache for app data
  • Error Handling - Typed error classes for every failure scenario
  • TypeScript - Full type definitions included
  • Project-scoped auth - Use one project secret key for customer reads; select an app only when needed

Initialization

const AppActor = require('@appmerge/appactor-sdk');

const client = new AppActor('sk_your_project_secret_key', {
  appId: 'your_app_id',                // optional, app-specific default
  baseUrl: 'https://api.appactor.com', // default
  timeout: 30000,                       // default, in ms
});

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | secretKey | string | Yes | Your project-scoped secret API key (sk_xxx) from Project Settings | | options.appId | string | No | Target AppActor app id for app-specific methods. Sent as X-AppActor-App-Id only when present | | options.baseUrl | string | No | API base URL | | options.timeout | number | No | Request timeout in ms (default: 30000) |

AppActor secret keys are project-scoped. Customer reads and token mutations use only sk + userId. App-specific operations such as app info require an app context. Use client.forApp(appId) when you need one.

client.forApp(appId)

Returns an app-scoped client with the same secret key, base URL, and timeout.

const appClient = client.forApp('your_app_id');
await appClient.apps.get();

Apps Service

client.apps.get(skipCache?)

Returns app information for the configured appId. Cached for 1 hour. Requires an app-scoped client.

const appClient = client.forApp('your_app_id');
const app = await appClient.apps.get();
// {
//   id: '1273925e-ba9e-4fe5-bd02-8c50a9c23ef8',
//   projectId: '3f7b3e1b-a98b-4d12-9cdf-6349930cda28',
//   name: 'Hair AI',
//   bundleId: 'net.appmerge.hairstyle',
//   packageName: null,
//   platform: 'ios'
// }

// Force fresh data (skip cache)
const freshApp = await appClient.apps.get(true);

client.apps.clearCache()

Clears cached app data. Returns true if cache was cleared.

client.apps.getCacheStats()

Returns cache hit/miss statistics.

const stats = client.apps.getCacheStats();
// { hits: 5, misses: 1, keys: 1, ksize: 0, vsize: 0 }

Subscribers Service

client.users.get(userId) / client.subscribers.getUser(userId)

Get full subscriber info including entitlements and subscriptions. Project-scoped clients return tokenBalance: null; use getTokenBalances for project token totals.

const subscriber = await client.users.get('appactor-anon-xxx');
// {
//   user: {
//     id: '019d4ef7-...',
//     appUserId: 'appactor-anon-xxx',
//     aliases: [...],
//     entitlements: {},
//     subscriptions: {},
//     nonSubscriptions: {},
//     tokenBalance: null,
//     firstSeenAt: '2026-04-02T16:11:59.195Z',
//     lastSeenAt: '2026-04-02T16:19:29.099Z',
//     trial: {
//       isTrial: true,
//       isCanceled: true,
//       willRenew: false,
//       activeTrialCount: 1,
//       canceledTrialCount: 1,
//       productId: 'weekly_trial',
//       expiresAt: '2026-04-09T16:11:59.195Z'
//     }
//   }
// }

Trial cancellation guard

user.trial is computed by the Node SDK from active subscriptions. A user is treated as an active trial when periodType === 'trial' and the subscription is still active. A trial is treated as canceled when AppActor sees autoRenew === false or unsubscribeDetectedAt.

const subscriber = await client.users.get('appactor-anon-xxx');

if (subscriber.user.trial.isTrial && subscriber.user.trial.isCanceled) {
  // Block trial-token spending or require an upgrade.
  return { allowed: false, reason: 'trial_canceled' };
}

client.users.getTokenBalances(userId)

Get the project token wallet for a user.

const balances = await client.users.getTokenBalances('user-id');
// {
//   currencyCode: 'tokens',
//   tokenBalance: { renewable: 30, nonRenewable: 10, total: 40 }
// }

client.subscribers.getTokens(userId)

Get the project token balance. This is a convenience alias that returns only tokenBalance.

const tokens = await client.subscribers.getTokens('user-id');
// { renewable: 30, nonRenewable: 0, total: 30 }

client.users.debitTokens(userId, amount, options?) / client.subscribers.debitTokens(...)

Debit tokens from a user's project wallet. By default, AppActor deducts from renewable tokens first, then non-renewable, atomically on the API side.

// User has: renewable=30, nonRenewable=10

// Debit 5 tokens
const balance = await client.users.debitTokens('user-id', 5, {
  idempotencyKey: 'request-123',
  metadata: { feature: 'video-gen' },
});
// Result: { renewable: 25, nonRenewable: 10, total: 35 }

// Force a specific bucket if needed
await client.users.debitTokens('user-id', 3, { tokenType: 'non_renewable' });

client.users.grantTokens(userId, amount, options) / client.subscribers.grantTokens(...)

Grant project tokens. tokenType is required for grants.

await client.users.grantTokens('user-id', 25, {
  tokenType: 'renewable',
  idempotencyKey: 'grant-request-123',
  metadata: { source: 'support-credit' },
});

client.subscribers.debitTokensByAppCost(userId, tokenCost?)

Debit tokens based on a cost value. Pass tokenCost to keep this project-scoped. If tokenCost is omitted, the SDK fetches the selected app's tokenCost, so that fallback requires client.forApp(appId).

// Project-scoped, no app id needed
await client.users.debitTokensByAppCost('user-id', 10);

// Optional app-cost lookup when you intentionally want app settings
const appClient = client.forApp('your_app_id');
await appClient.subscribers.debitTokensByAppCost('user-id');

Error Handling

Every error extends AppActorError with a code property for programmatic handling.

const AppActor = require('@appmerge/appactor-sdk');

try {
  await client.users.debitTokens('user-id', 100);
} catch (error) {
  if (error instanceof AppActor.InsufficientTokensError) {
    console.log(`Need ${error.required}, have ${error.available}`);
  } else if (error instanceof AppActor.SubscriberNotFoundError) {
    console.log(`User not found: ${error.subscriberId}`);
  } else if (error instanceof AppActor.ApiError) {
    console.log(`API error ${error.statusCode}: ${error.message}`);
  }
}

Error Classes

| Error | Code | Properties | When | |-------|------|------------|------| | AppActorError | varies | code | Base class for all errors | | AppNotFoundError | APP_NOT_FOUND | appId | App not found | | AppIdRequiredError | APP_ID_REQUIRED | operation | App-specific method called without app context | | SubscriberNotFoundError | SUBSCRIBER_NOT_FOUND | subscriberId | User not found | | InsufficientTokensError | INSUFFICIENT_TOKENS | subscriberId, required, available | Not enough tokens | | InvalidTokenCostError | INVALID_TOKEN_COST | tokenCost | Token cost is 0 or invalid | | ApiError | API_ERROR | statusCode, body | HTTP error from API |

Complete Example

const AppActor = require('@appmerge/appactor-sdk');

const client = new AppActor(process.env.APPACTOR_SECRET_KEY);

async function processUserAction(userId) {
  try {
    // Check token balance
    const tokens = await client.users.getTokens(userId);
    const tokenCost = 1;

    if (tokens.total < tokenCost) {
      return { success: false, reason: 'insufficient_tokens', balance: tokens };
    }

    // Debit tokens
    const newBalance = await client.users.debitTokensByAppCost(userId, tokenCost, {
      idempotencyKey: `consume:${userId}:${Date.now()}`,
      metadata: { feature: 'video-gen' },
    });

    return { success: true, balance: newBalance };

  } catch (error) {
    if (error instanceof AppActor.SubscriberNotFoundError) {
      return { success: false, reason: 'user_not_found' };
    }
    throw error;
  }
}

// Cleanup when shutting down
process.on('SIGTERM', () => client.destroy());

Constants

const { TOKEN_TYPES } = require('@appmerge/appactor-sdk');

TOKEN_TYPES.AUTO          // 'auto'
TOKEN_TYPES.RENEWABLE     // 'renewable'
TOKEN_TYPES.NON_RENEWABLE // 'non_renewable'

TypeScript

Full type definitions are included. Works out of the box with TypeScript projects.

import AppActor, {
  App,
  Subscriber,
  TokenBalance,
  ProjectTokenBalances,
  SubscriberNotFoundError,
  InsufficientTokensError,
} from '@appmerge/appactor-sdk';

const client = new AppActor(process.env.APPACTOR_SECRET_KEY!);

const subscriber: Subscriber = await client.users.get('user-id');
if (subscriber.user.trial.isTrial && subscriber.user.trial.isCanceled) {
  throw new Error('Trial was canceled before renewal.');
}

const balances: ProjectTokenBalances = await client.users.getTokenBalances('user-id');
const tokens: TokenBalance = await client.users.getTokens('user-id');
const newBalance: TokenBalance = await client.users.debitTokens('user-id', 5);

Requirements

  • Node.js >= 18.0.0 (uses native fetch)
  • An AppActor project-scoped secret API key (sk_xxx)
  • The target AppActor app id only for app-specific operations such as apps.get()

License

ISC