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

@linkerestate/sdk

v1.4.0

Published

Official client for the Linkerestate API: agent rotation, lead capture and property catalog.

Readme

@linkerestate/sdk

Official client for the Linkerestate API.

npm i @linkerestate/sdk      # or: pnpm add / yarn add / bun add

Zero runtime dependencies. ESM only, Node 18+ and any modern browser.

Entry points

Each surface has its own module and its own key scope.

| Import | Key scope | Runs in | | --- | --- | --- | | @linkerestate/sdk/client | campaign_rr — public | browser | | @linkerestate/sdk/server | campaign_leads — secret | server | | @linkerestate/sdk/catalog | api_v1 — secret | server |

Issue keys under Settings → Developer in your workspace.

Agent rotation (browser)

Points your WhatsApp / phone CTAs at whichever agent is on turn, and records the click. Both calls fail soft: if the API is unreachable the buttons keep the hrefs your page rendered.

import { createRoundRobinClient } from "@linkerestate/sdk/client"

const rr = createRoundRobinClient({
  campaignId: "00000000-0000-0000-0000-000000000000",
  apiKey: "wak_rr_…",          // safe to ship to the browser
  source: "…",                 // optional: campaign source webhook_key, for attribution
})

const agent = await rr.peek()
if (agent?.waUrl) document.querySelector("#wa-btn")?.setAttribute("href", agent.waUrl)

const detach = rr.attachCommitHandlers()   // binds clicks; returns a cleanup fn

Lead-form lane

A separate rotation with its own cursor — a lead never spends a WhatsApp turn, nor the other way round. Use it when a page has no agent of its own: render the card, then pass the id on submit so that same person receives the lead.

const turn = await rr.peekLeadForm()
// { leadFormAgentId: "…", agent: { id, username, full_name, avatar } | null }

agent is null when the profile is not public: the lead still routes to them, but there is no card to show. Peeking spends nothing — the turn is only consumed when the lead arrives carrying lead_form_agent_id.

attachCommitHandlers() binds to [id^="wa-btn"], [data-rr-link="whatsapp"] and [id^="tel-btn"], [data-rr-link="tel"], and is safe to call again after a client-side navigation — each element is only bound once.

Lead capture (server)

import { createLeadsClient } from "@linkerestate/sdk/server"

const leads = createLeadsClient({
  campaignId: "00000000-0000-0000-0000-000000000000",
  apiKey: process.env.LINKERESTATE_LEADS_KEY!,   // never expose this
  webhookKey: process.env.LINKERESTATE_WEBHOOK_KEY!,
})

const { assigned_to } = await leads.submit({
  first_name: "Ana",
  phone: "+18095551234",
  email: "[email protected]",
  // Optional: the agent whose card the page rendered, from peekLeadForm().
  // Spends that rotation turn, so the next visitor sees the following agent.
  lead_form_agent_id: turn?.leadFormAgentId,
})

A lead needs at least one of first_name, last_name, phone, email.

Catalog (server)

import { createCatalogClient } from "@linkerestate/sdk/catalog"

const catalog = createCatalogClient({ apiKey: process.env.LINKERESTATE_API_KEY! })

const { data, meta } = await catalog.properties({ page: 1, listing_type: "sale" })

// `page_size` is honoured up to 100 and reported back in `meta.page_size`. Omit
// it for the endpoint's own default of 10 (25 for agents). Ask for it when you
// need the whole filtered set: walking a large catalogue ten rows at a time is
// many requests where one or two would do, and every one of them is spent
// against this surface's rate-limit budget.
const wide = await catalog.properties({ page: 1, page_size: 100 })

// One property by UUID, numeric public id, or slug
const { data: property } = await catalog.property("torre-central")

// Ranked by sales instead of by name. Amounts are never exposed — only the
// ordering. Heavier than the default: this path pages in memory.
const ranked = await catalog.agents({ sort: "sales" })
// `meta.total_active` counts the workspace's ACTIVE advisors, published profile
// or not — unlike `meta.total`, which counts what this response returns and so
// only the public ones. Use it for a headcount; use `total` to paginate.

if (ranked.meta.sort_fallback) {
  // The ranking query failed and the list came back shuffled — deliberately
  // not alphabetical, which would always favour names starting with "A".
  // Don't present it as ranked.
}

// One agent by UUID or username. Public URLs carry the username, never the id.
const { data: agent } = await catalog.agent("maria-perez")

// Append-only popularity event. Un-favouriting does not decrement, so there is
// no counterpart call.
await catalog.favorite(property.id, { visitorId })

An agent whose profile stopped being public answers 404 exactly like one who never existed, so a lookup used as a fallback should read a LinkerestateError with status: 404 as "no agent" rather than as a failure.

Caching

Every request goes out no-store by default. That is right for a rotation and wrong for a catalog: under Next.js it opts the whole page out of static rendering. Pass fetchOptions to put the catalog back on ISR — client-wide, or per call when one endpoint wants a different window:

const catalog = createCatalogClient({
  apiKey: process.env.LINKERESTATE_API_KEY!,
  fetchOptions: { cache: "force-cache", next: { revalidate: 600 } },
})

// This one list tolerates a much longer window than the rest.
const counts = await catalog.properties({ page: 1 }, {
  fetchOptions: { next: { revalidate: 1800 } },
})

headers, body and signal are not overridable — the first two describe the request and the third enforces the SDK's timeout.

Property and agent objects are returned as-is from the API and typed as open records.

Errors

Every non-2xx response throws a LinkerestateError carrying status and the parsed body — except peek(), which returns null instead so a rotation outage can never break a page.

import { LinkerestateError } from "@linkerestate/sdk/server"

try {
  await leads.submit({ email: "[email protected]" })
} catch (err) {
  if (err instanceof LinkerestateError && err.status === 401) {
    // revoked key, or a key issued for a different workspace
  }
}

Pointing at another environment

baseUrl overrides the API root. Leave it unset unless you are pointing at a non-production environment.

createCatalogClient({ apiKey, baseUrl: process.env.LINKERESTATE_API_URL })

License

MIT