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

@medalbot/render

v0.1.6

Published

Await finished Counter-Strike skin renders with automatic retries and polling

Downloads

1,113

Readme

TypeScript client

Await client.render() to get image URLs. Cache hits and new renders return the same image map. The SDK submits the request, waits for completion and handles transient transport retries.

npm install @medalbot/render
import { RenderClient } from '@medalbot/render';

const client = new RenderClient({ apiKey });
const images = await client.render({ inspectUrl });

console.log(images.preview);
console.log(images.front);
console.log(images.back);

The default API URL is https://render.medalbot.cc. Supply baseUrl for local development. The package includes its public protocol types and has no private workspace dependencies.

For a StatTrak weapon, await client.render({ inspectUrl, statTrak: 1337 }) overrides the kill counter in the image. The integer range is 0 to 999999; zero is an explicit override. Omit statTrak to preserve the inspected counter. This only changes the render, never the Steam item. A weapon without StatTrak returns stat_trak_unavailable.

Render appearance

appearance is optional. Omit it or send "default" to preserve the original lighting. Use "pattern" for the alternative lighting:

const images = await client.render({
  inspectUrl,
  appearance: "pattern",
});

The API supports pattern appearance for AK-47 preview, front, and back images. Omit views to request all three, or provide a subset. Pattern appearance preserves the original preview lighting. Unsupported items return HTTP 422 with code unsupported_appearance. The server determines availability; the SDK forwards your requested views without changing them.

Waiting and retries

The initial HTTP request asks the server to wait up to ten seconds. A ready result returns immediately. For longer jobs, the SDK polls automatically, starting at 500 milliseconds and backing off to two seconds. The caller's promise resolves only when the requested images are ready.

const images = await client.render(
  { inspectUrl, views: ['front'], format: 'webp' },
  {
    idempotencyKey: 'listing-123-front-v1',
    timeoutMs: 120_000,
    signal: abortController.signal,
  },
);

The default timeout is two minutes for submission and polling together. Cancelling or timing out stops this caller's wait; shared GPU work continues. A timeout from render() throws a RenderError with code wait_timeout, the admitted renderId when known and the idempotencyKey. Resume with client.renders.wait(error.renderId), or retry client.render() with that idempotency key if submission timed out before an ID arrived.

client.renders.create/get/wait/batch/lookup are lower-level operations for callers that need to persist request IDs or manage background work. Normal callers only need render().

Transport choice

Replicate's JavaScript client exposes an awaited result and uses a bounded blocking request with a polling fallback. fal's client also exposes an awaited call that returns generated images. Our SDK uses the same caller-facing pattern over HTTP and JSON.

The result is a set of complete image files. It has no token stream to consume. SSE could add optional progress events later; it is not required to await completion. gRPC is unnecessary for this public HTTP API.

Post the link early

A bot can post an image link before the render finishes. Submit without waiting, with links: true, and use links.front at once:

const render = await client.renders.create(
  { inspectUrl, views: ["front"] },
  { links: true, idempotencyKey: "listing-123-front-v1" },
);
const link = "links" in render ? render.links?.front : undefined;
if (link) {
  await postEmbed(link);
}

The link is final at admission. The image host holds a request for a pending link for up to 20 seconds and answers with the image as soon as it exists, so a chat embed that fetches it a few seconds later gets the image instead of a 404. A render that takes longer than the hold, or fails, answers 503 with Retry-After: 2; wait for the finished images instead if your client keeps that response. A completed render's links equal its images, and a failed render has no links. Completion still depends on images, so render() and renders.wait() keep returning finished images.

links: true on renders.create, get, batch and lookup sends Prefer: links, and each batch or lookup item carries its own links. Until the API enables links, it ignores the preference and the field is absent, so handle a missing link. Once the API returns links by default, the field is present without the option and the option changes nothing. If the API admits the render but cannot prepare its links, it answers 503 render_links_unavailable with Retry-After. The SDK retries it with the same idempotency key, like other 503 responses. Response parsing ignores fields this SDK version does not know, so a field the API adds later does not break it.

Image delivery

import { imageUrl } from "@medalbot/render";

const card = imageUrl(images.front, "card");
const zoom = imageUrl(images.front, "zoom");

The public recipes trim transparent padding, treating alpha values above 8 as visible. small fits within 500×220 at WebP quality 85, card fits within 1000×440 at quality 90, and zoom keeps the cropped native dimensions at quality 95. Resizing preserves the aspect ratio and never enlarges the image. The original lossless image stays unchanged.

Helpers accept an absolute HTTP(S) original URL under /v1/<canonical SHA-256>.webp or .png. They reject credentials, queries, fragments, unknown variants and derivative URLs. They preserve the original origin for custom image domains and make no network request.

For a responsive card, choose a smaller image on narrow screens without downloading the original:

<picture>
  <source media="(max-width: 600px)" srcSet={imageUrl(images.front, "small")} />
  <img src={imageUrl(images.front, "card")} alt="Weapon front view" loading="lazy" decoding="async" />
</picture>

In a zoom dialog, show the card first, then replace it after the zoom image decodes. Start this on user intent so opening a gallery does not download every zoom image:

async function showZoom(element: HTMLImageElement, original: string) {
  const card = imageUrl(original, "card");
  element.src = card;
  const larger = new Image();
  larger.src = imageUrl(original, "zoom");
  try {
    await larger.decode();
    if (element.isConnected && element.src === card) {
      element.src = larger.src;
    }
  } catch {
    // Keep the card if the larger image cannot load.
  }
}

Keep the original URL for an explicit lossless download. It is separate from the smaller display images.