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

@heatloop/sdk

v0.1.1

Published

Typed Node.js client for the Heatloop API. Wraps auth, polling, webhook verification, SSE, and the rating loop with end-to-end response typing.

Readme

@heatloop/sdk

Typed Node.js client for the Heatloop API. Wraps auth, adaptive polling, webhook verification, SSE, and the mandatory rating loop — with end-to-end response typing keyed on your own response_schema.

npm

npm install @heatloop/sdk

Requires Node 20+.


Contents


Quickstart

import { Heatloop, defineSchema, score, verdict } from "@heatloop/sdk";

const heatloop = new Heatloop({ apiKey: process.env.HEATLOOP_API_KEY! });

const submission = await heatloop.tasks.submit({
  type: "ui-review",
  prompt: "Review this landing page.",
  responseSchema: defineSchema({
    visual_hierarchy: score(1, 10),
    typography: score(1, 10),
    verdict: verdict(),
  }),
  skillTags: ["ui-review"],
  budgetPerResponse: 1.0,
});

const result = await submission.waitForResult();

console.log(result.responses[0].scores.visual_hierarchy); // typed: number
console.log(result.responses[0].verdict);                 // typed: "pass" | "fail" | "conditional"

await result.responses[0].rate("useful");

Schemas

Schema builders generate the response_schema wire format and thread the result type all the way through to result.responses[i] — no manual generics.

import {
  boolean, defineSchema, flags, multiselect,
  number, score, select, text, verdict,
} from "@heatloop/sdk";

const schema = defineSchema({
  visual_hierarchy: score(1, 10),
  typography: score(1, 10),
  contrast_pass: boolean(),
  primary_cta: select(["click", "scroll", "ignore"] as const),
  flags_seen: flags(["broken-link", "low-contrast", "missing-alt"] as const),
  verdict: verdict(),
  notes: text({ maxLength: 280 }),
});

Each builder returns a strongly-typed SchemaField; defineSchema collects them into a Schema whose InferResponse<T> you can use anywhere.

Polling

submission.waitForResult() polls GET /v1/tasks/:id with the API's retry_after_seconds hint, falling back to an adaptive cadence that gets patient as the SLA approaches. Cancel with an AbortSignal:

const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 60_000);

const result = await submission.waitForResult({ signal: ctrl.signal });

If the task expires, is cancelled, or hits a terminal API error, the promise rejects with a typed error (TaskExpired, TaskCancelled, RateLimited, …).

Rating loop

Every response must be rated within 24 hours — the rating drives Looper reputation. unusable requires a reason both at the type layer and at runtime:

await response.rate("useful");
await response.rate("partial",  { reason: "Missed the brand voice." });
await response.rate("unusable", { reason: "Wrong domain." });
// await response.rate("unusable"); // ← fails to compile

// After 24h, rate() throws RatingWindowExpired.

Multi-respondent consensus

Tasks with maxRespondents > 1 get an aggregation payload on the terminal result that lets you act on the cohort without inspecting every response:

const result = await submission.waitForResult();

if (result.aggregation) {
  const agg = result.aggregation;
  console.log(agg.verdict_consensus);    // "pass" | "fail" | "conditional"
  console.log(agg.verdict_agreement);    // 0–1, share matching the consensus
  console.log(agg.consensus_confidence); // "high" | "medium" | "low"
  console.log(agg.consensus_score);      // 0–1, composite of agreement + score coherence

  // Per-dimension stats
  console.log(agg.score_stats.quality);  // { n, mean, median, min, max, stddev }

  // Which flags multiple loopers raised
  for (const [flag, { count, share }] of Object.entries(agg.flags_raised)) {
    if (share >= 0.5) console.log(`${flag} flagged by majority (${count})`);
  }
}

The composite score weights verdict agreement at 60% and per-dimension score coherence at 40%. Treat low confidence as "needs human review of the raw responses" rather than as a weak signal — it specifically catches genuinely split cohorts.

Templates

For repeated task shapes, sponsors save a template once and agents reference it by id:

const submission = await heatloop.tasks.submit({
  templateId: "tpl_ui_review",
  context: "SaaS landing page",
  attachments: [{ type: "image/png", url: "https://...", label: "shot", size_bytes: 240_000 }],
});

For programmatic management (CI scripts):

const templates = await heatloop.sponsor.templates.list();
const tpl = await heatloop.sponsor.templates.create({
  name: "UI Review",
  type: "ui-review",
  prompt: "Review this design.",
  responseSchema: schema,            // a defineSchema(...) result, or raw wire shape
  defaultSkillTags: ["ui-review"],
  defaultBudgetPerResponse: 1.0,
});
await heatloop.sponsor.templates.delete(tpl.id);

Auth caveat: /v1/sponsor/templates endpoints currently authenticate against a sponsor session (cookie or, in dev, the X-Sponsor-Id header), not the agent API key. Pass session credentials via requestOptions.headers until the API hardens to accept Bearer keys on sponsor routes. The agent-facing path (tasks.submit({ templateId })) uses the API key directly and is unaffected.

Server-Sent Events

For callback_mode: "sse" tasks (or alongside polling), tasks.events() returns an async iterable over typed events:

const ctrl = new AbortController();

for await (const event of heatloop.tasks.events(taskId, { signal: ctrl.signal })) {
  switch (event.type) {
    case "task.status":
      console.log(`Initial status: ${event.payload.status}`);
      break;
    case "task.response_received":
      console.log(`Response ${event.payload.response_id} received`);
      break;
    case "task.completed":
      console.log("Done.");
      break;
  }
}
// Iteration ends on a terminal event (completed / cancelled / expired)
// or when ctrl.abort() fires.

Auto-reconnects with exponential backoff (default 5 attempts) on transient drops; 4xx errors propagate immediately. Pass { reconnect: { attempts: 1 } } to disable.

Webhooks

callback_mode: "webhook" deliveries are signed with HMAC-SHA256 over the raw JSON body, keyed on the per-task webhook_secret returned at submission time (submission.webhookSecret). Use constructEvent to verify and parse in one call:

import express from "express";
import { Heatloop, WebhookSignatureError } from "@heatloop/sdk";

const heatloop = new Heatloop({ apiKey: process.env.HEATLOOP_API_KEY! });
const app = express();

app.post(
  "/heatloop",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      const event = heatloop.webhooks.constructEvent({
        rawBody: req.body,
        signature: req.header("x-heatloop-signature") ?? "",
        eventType: req.header("x-heatloop-event") ?? "",
        secret: process.env.HEATLOOP_WEBHOOK_SECRET!,
      });

      if (event.type === "task.completed") {
        // event.payload is typed via the discriminated union
        console.log(`Task ${event.payload.task_id} completed.`);
      }

      res.status(200).end();
    } catch (err) {
      if (err instanceof WebhookSignatureError) {
        return res.status(400).json({ reason: err.reason });
      }
      throw err;
    }
  },
);

The same constructEvent works for Fastify (@fastify/raw-body), bare Node (req stream → buffer), Hono, etc. — anywhere you can give it the raw bytes and the two headers.

For a boolean-only check without parsing, use heatloop.webhooks.verify({ rawBody, signature, secret }).

Errors

All thrown errors inherit from HeatloopError. Catch the base for blanket handling, or narrow on a specific class:

| Class | When | | ----------------------- | ---------------------------------------------------- | | ValidationError | 400 — request shape rejected | | Unauthorized | 401 — missing/invalid API key | | Forbidden | 403 — key lacks permission | | BudgetExhausted | 402 — sponsor's monthly budget cap hit | | NotFound | 404 — task / template id unknown | | Conflict | 409 — concurrent state change | | RateLimited | 429 — surfaces retryAfterSeconds | | ServerError | 5xx — retryable; SDK retries automatically | | TaskExpired | terminal — task hit its expiry without responses | | TaskCancelled | terminal — task was cancelled | | RatingWindowExpired | response.rate() called past the 24h window | | WebhookSignatureError | signature mismatch / missing secret / replay |

import { BudgetExhausted, RateLimited } from "@heatloop/sdk";

try {
  await heatloop.tasks.submit({ ... });
} catch (err) {
  if (err instanceof BudgetExhausted) return alertSponsor();
  if (err instanceof RateLimited)     return setTimeout(retry, err.retryAfterSeconds * 1000);
  throw err;
}

Configuration

const heatloop = new Heatloop({
  apiKey: process.env.HEATLOOP_API_KEY!,
  baseUrl: "https://api.heatloop.ai", // default
  timeout: 30_000,                    // per-request ms, default 30s
  retry: {
    attempts: 4,                      // total tries; { attempts: 1 } disables
    baseDelayMs: 1_000,               // first retry wait, doubles per attempt with jitter
    maxDelayMs: 30_000,               // cap on per-attempt wait
  },
  fetch: customFetch,                 // override (e.g. undici, polyfill)
});

429 responses honor Retry-After; 5xx uses exponential backoff with full jitter. 400-class errors short-circuit retries.


Why an SDK

The Heatloop REST API is straightforward, but every TypeScript agent ends up rebuilding the same plumbing: adaptive polling, HMAC-SHA256 webhook verification, the mandatory rating loop, and end-to-end response typing keyed on the agent's own response_schema. This package ships those once for the whole ecosystem.

License

MIT