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

erlc-v2

v1.0.1

Published

Premium, lightweight JavaScript wrapper for the ER:LC API v2.

Readme

erlc-v2

JavaScript client for the ER:LC API v2.

Built for Node 18+.

Stable Release

This wrapper is now on a stable release track (1.0.0+).

Responsibility and API Safety

You are responsible for how you use this wrapper and API key(s). If you use it recklessly (for example, aggressive request spam, ignoring rate limits, or abusive automation) and get rate-limited, blocked, or banned by PRC/Cloudflare, that is on you.

This project is provided as-is. Always monitor your integration and respect PRC API rules and headers.

Install

npm install erlc-v2

Quick Start (CommonJS)

const { Client } = require("erlc-v2");

const client = new Client({
  serverKey: "YOUR_SERVER_KEY",
  // globalKey: "YOUR_GLOBAL_KEY", // optional
});

client.on("disconnect", ({ reason, error }) => {
  console.error("Disconnected:", reason, error?.message);
});

async function main() {
  const snapshot = await client.server.fetch({
    players: true,
    staff: true,
    queue: true,
  });

  console.log("Server:", snapshot.name);
  console.log("Players:", `${snapshot.currentPlayers}/${snapshot.maxPlayers}`);
  console.log("Players payload:", snapshot.players.length);
}

main()
  .catch(console.error)
  .finally(() => client.destroy());

Quick Start (ESM)

import { Client } from "erlc-v2";

const client = new Client({
  serverKey: "YOUR_SERVER_KEY",
});

Options

new Client({
  serverKey: string, // required
  globalKey?: string, // optional
  logging?: boolean, // default: false
  logger?: { info, warn, error, debug },
  cache?: {
    enabled?: boolean, // default: true
    ttlMs?: number, // default: 1500
    maxSize?: number, // default: 500
    provider?: "memory" | "redis", // default: auto (redis if redis config is present)
    redisUrl?: string, // optional (requires `npm i redis`)
    redisPrefix?: string, // default: "erlc-v2:cache"
    redisClient?: object, // optional pre-configured Redis client
  },
  rateLimit?: {
    enabled?: boolean, // default: true
    strictSerial?: boolean, // default: true (global one-at-a-time queue)
    bucketLimit?: number, // default: 1
    totalLimit?: number, // default: 1
    unauthLimit?: number, // default: 3
  },
  polling?: {
    enabled?: boolean, // default: true
    intervalMs?: number, // default: 2500 (min enforced: 250)
    bypassCache?: boolean, // default: true
  },
});

Legacy aliases (perBucketConcurrency, globalConcurrency, unauthorizedThreshold) are still accepted.

Redis Cache (Optional)

You can use Redis instead of in-memory cache by passing either cache.redisUrl or cache.redisClient.

If you use redisUrl, install the Redis client package:

npm i redis

Example with connection URL:

const { Client } = require("erlc-v2");

const client = new Client({
  serverKey: "YOUR_SERVER_KEY",
  cache: {
    provider: "redis",
    redisUrl: "redis://localhost:6379",
    redisPrefix: "myapp:erlc",
    ttlMs: 2000,
  },
});

Example with your own Redis client instance:

const { createClient } = require("redis");
const { Client } = require("erlc-v2");

(async () => {
  const redis = createClient({ url: process.env.REDIS_URL });
  await redis.connect();

  const client = new Client({
    serverKey: "YOUR_SERVER_KEY",
    cache: {
      redisClient: redis,
      redisPrefix: "myapp:erlc",
    },
  });
})();

API

Core:

  • await client.server.fetch(flags, requestOptions?)
  • client.destroy()
  • client.cache.clear()

Convenience methods:

  • await client.players.list()
  • await client.map.render(options?)
  • await client.map.renderUser(userId, options?)
  • await client.staff.list()
  • await client.logs.kills()
  • await client.logs.joins()
  • await client.logs.commands()
  • await client.logs.modCalls()
  • await client.vehicles.list()
  • await client.queue.get()
  • await client.commands.execute(":h Hey everyone!") (v1 endpoint)

Fetch Flags

  • players -> Players
  • staff -> Staff
  • joinLogs -> JoinLogs
  • queue -> Queue
  • killLogs -> KillLogs
  • commandLogs -> CommandLogs
  • modCalls -> ModCalls
  • vehicles -> Vehicles

Request Options

  • bypassCache?: boolean
  • cacheTtlMs?: number
  • dedupe?: boolean

Command Execution (v1)

client.commands.execute(command) sends a POST request to /v1/server/command. Command execution is FIFO-queued client-side, so commands run one-at-a-time in order.

Blocked by client policy (request will be rejected before hitting the API):

  • :view
  • :to
  • :tocar
  • :toatv
  • :logs
  • :mods
  • :admins
  • helpers / :helpers
  • :administrators
  • :moderators
  • :killlogs
  • :kl
  • :cmds
  • :commands

Example:

await client.commands.execute(":h Hey everyone!");
await client.commands.execute(":log recentban He was trolling!");

Map Rendering

Render an ER:LC map (3121x3121) with player markers that use Roblox avatars.

const result = await client.map.render();

// png buffer
console.log(result.buffer);
console.log(result.players.length);

client.map.render() renders the full map with all players currently in the server.

Render an official season/type map preset:

const fallBlank = await client.map.render({
  season: "fall",
  type: "blank",
});

const fallPostals = await client.map.render({
  season: "fall",
  type: "postals",
});

const winterBlank = await client.map.render({
  season: "winter", // alias: "snow"
  type: "blank",
});

const winterPostals = await client.map.render({
  season: "winter",
  type: "postals",
});

Use your own map image URL:

const customMap = await client.map.render({
  mapUrl: "https://example.com/my-map.png", // mainly used in cases where we fail to add the most recent map when its released (sizing should be 3121x3121)
});

Render only one player by Roblox user ID:

const single = await client.map.renderUser(123456789, {
  season: "winter",
  type: "postals",
});

Options:

  • userId?: number | string
  • userIds?: Array<number | string>
  • players?: any[] (use your own pre-fetched player payload)
  • mapUrl?: string (custom map URL; if set, this overrides season/type)
  • season?: string ("fall" or "winter"/"snow" for official presets)
  • type?: string ("blank" or "postals" for official presets)
  • mapSeason?: string (alias of season)
  • mapType?: string (alias of type)
  • coordinateBounds?: { minX, maxX, minY, maxY, invertY? }
  • clampToMap?: boolean (default: true)
  • robloxHeadshotSize?: string (default: "150x150")
  • marker?: { outerRadius, innerRadius, tipLength, tipWidth, fillColor, shadow }

Map size is fixed to 3121x3121.

Result shape:

  • buffer (image/png)
  • map ({ url, season, type, width, height })
  • players (rendered marker metadata)
  • skipped (players skipped because coordinates were unavailable/invalid)
  • requestedUserIds (IDs requested via userId / userIds)
  • unmatchedUserIds (requested IDs not found in current player payload)

Events

  • ready
  • playerJoin
  • playerLeave
  • kill
  • vehicleSpawn
  • vehicleDespawn
  • queueUpdate
  • staffUpdate
  • modCall
  • commandLog
  • logCommand (only when command starts with :log)
  • serverUpdate
  • error
  • disconnect ({ reason, error })

Alias event names are also supported with client.on(...):

  • onReady
  • onJoin
  • onLeave
  • onKill
  • onVehicleSpawn
  • onVehicleDespawn
  • onQueueUpdate
  • onStaffUpdate
  • onModCall
  • onCommandLog
  • onLogCommand
  • onServerUpdate
  • onError
  • onDisconnect

Shortcut methods are available too:

client.onJoin((payload) => console.log("join", payload));
client.onLeave((payload) => console.log("leave", payload));
client.onVehicleSpawn((payload) => console.log("spawn", payload));
client.onLogCommand(({ command, parsed }) => {
  console.log("raw command:", command.Command);
  console.log("keyword:", parsed.keyword);
  console.log("args:", parsed.args);
});

logCommand / onLogCommand fires when a command starts with :log.

Events are deduped per poll cycle so the same log entry is not emitted repeatedly.

Rate Limits

Requests are automatically bucketed using API response headers:

  • X-RateLimit-Bucket
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset

On 429, the client blocks the affected bucket until retry time/reset.

By default, requests are strictly serialized (strictSerial: true) so this client does not send parallel requests. This is intentionally conservative for anti-abuse/rate-limit safety.

Errors

The client normalizes errors into classes:

  • ERLCError
  • ERLCHttpError
  • ERLCAPIError
  • RateLimitError
  • KeyExpiredError (2002)
  • KeyBannedError (2004)
  • InvalidGlobalKeyError (2003)
  • ServerOfflineError (3002)
  • RestrictedError (9998)
  • ModuleOutOfDateError (9999)

Terminal key errors (2002, 2004) trigger disconnect and stop polling.

Repeated 403 responses can also trigger disconnect (reason: "unauthorized").

Notes

  • API base URL: https://api.policeroleplay.community
  • Server-Key is required for requests
  • Authorization is optional (globalKey)