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

suzi-pnl-card-generator

v1.4.0

Published

Generate PnL and performance cards for trading, prediction markets, and custom products

Readme

suzi-pnl-card-generator

Generate polished PnL and performance card images. Outputs PNG or SVG. Built with Satori + resvg.

The package supports old protocol-specific inputs, a generic performance primitive for unsupported products like sports bets, grid bots, options, spot trades, LP positions, portfolio snapshots, and custom venues, plus dedicated pair_trade and basket_trade cards for multi-leg trades.

Install

bun add suzi-pnl-card-generator

Quick Start

import { generateCard } from "suzi-pnl-card-generator";
import { writeFileSync } from "fs";

const png = await generateCard({
  type: "performance",
  platform: { name: "Phoenix" },
  subject: { title: "SOL Grid Bot", subtitle: "61 day strategy" },
  result: { label: "Total PnL", value: "+7.46%", profit: true },
  stats: [
    { label: "Grid PnL", value: "+$29.01" },
    { label: "Unrealized", value: "+$112.05" },
    { label: "Entry", value: "$88.80" },
    { label: "Current", value: "$94.07" }
  ]
});

writeFileSync("card.png", png);

API

generateCard(data, options?)

Returns Promise<Buffer> for PNG or Promise<string> for SVG.

Card Types

Supported type values:

"performance" // generic slot-based card
"pair_trade"  // strict two-leg long/short trade card
"basket_trade" // two-to-six-leg basket trade card
"perp"        // perp convenience wrapper
"prediction"  // prediction-market convenience wrapper
"phoenix"     // Phoenix convenience alias
"hyperliquid" // legacy compatible
"polymarket"  // legacy compatible

The renderer does not calculate PnL, ratios, hedge weights, or exposure. Consumers should pass already-formatted display values.

Render Options

| Field | Type | Default | Description | |---|---|---:|---| | format | "png" \| "svg" | "png" | Output format | | scale | number | 2 | PNG scale factor | | hd | boolean | true | Full-resolution bundled assets |

Generic Performance Cards

Use type: "performance" when the consumer owns the product semantics and the package should only render a card.

await generateCard({
  type: "performance",
  platform: {
    name: "Phoenix",
    icon: "data:image/png;base64,..." // optional
  },
  subject: {
    title: "SOL Grid Bot",
    subtitle: "61 day strategy",
    icon: "data:image/png;base64,..." // optional
  },
  result: {
    label: "Total PnL",
    value: "+7.46%",
    profit: true
  },
  stats: [
    { label: "Grid PnL", value: "+$29.01" },
    { label: "Unrealized", value: "+$112.05" },
    { label: "Entry", value: "$88.80" },
    { label: "Current", value: "$94.07" }
  ]
});

Sports Bet

await generateCard({
  type: "performance",
  platform: { name: "Polymarket" },
  subject: { title: "Lakers vs Warriors", subtitle: "Lakers ML" },
  result: { label: "PnL", value: "+32.80%", profit: true },
  stats: [
    { label: "Entry", value: "62c" },
    { label: "Exit", value: "82c" },
    { label: "Stake", value: "$250" },
    { label: "Profit", value: "+$82" }
  ]
});

Options Position

await generateCard({
  type: "performance",
  platform: { name: "Deribit" },
  subject: { title: "BTC 110K Call", subtitle: "Expires Jun 28" },
  result: { label: "PnL", value: "-18.40%", profit: false },
  stats: [
    { label: "Strike", value: "$110K" },
    { label: "Entry", value: "$2,140" },
    { label: "Mark", value: "$1,746" },
    { label: "Size", value: "0.5 BTC" }
  ]
});

Custom Protocol Logo

import { readFileSync } from "fs";

const platformIcon = `data:image/png;base64,${readFileSync("./drift.png").toString("base64")}`;
const tokenIcon = `data:image/png;base64,${readFileSync("./sol.png").toString("base64")}`;

await generateCard({
  type: "performance",
  platform: { name: "Drift", icon: platformIcon },
  subject: { title: "SOL-PERP", icon: tokenIcon },
  result: { label: "PnL", value: "+12.40%", profit: true },
  stats: [
    { label: "Entry", value: "$140.10" },
    { label: "Exit", value: "$157.48" },
    { label: "Leverage", value: "10x" }
  ]
});

Pair and Basket Trade Cards

Use type: "pair_trade" when the position is exactly two legs and type: "basket_trade" when it has 2-6 legs. Each leg accepts an optional icon; if it is omitted, the renderer draws a deterministic symbol badge so the card still has asset identity without requiring this package to fetch token images.

Pair Trade

await generateCard({
  type: "pair_trade",
  platform: { name: "Phoenix", icon: "data:image/png;base64,..." },
  title: "SOL / ETH",
  subtitle: "Mean reversion",
  result: { label: "Net PnL", value: "+8.42%", profit: true },
  legs: [
    {
      side: "long",
      symbol: "SOL",
      icon: "data:image/png;base64,...",
      pnl: "+$312",
      profit: true,
      weight: "55%",
      entry: "$140.10",
      exit: "$157.48"
    },
    {
      side: "short",
      symbol: "ETH",
      icon: "data:image/png;base64,...",
      pnl: "+$108",
      profit: true,
      weight: "45%",
      entry: "$3,420",
      exit: "$3,280"
    }
  ],
  stats: [
    { label: "Entry Ratio", value: "0.0472" },
    { label: "Exit Ratio", value: "0.0511" }
  ]
});

Basket Trade

await generateCard({
  type: "basket_trade",
  platform: { name: "Phoenix", icon: "data:image/png;base64,..." },
  title: "SOL / ETH / BTC",
  subtitle: "Market neutral basket",
  result: { label: "Net PnL", value: "+6.18%", profit: true },
  legs: [
    { side: "long", symbol: "SOL", icon: "data:image/png;base64,...", pnl: "+$312", profit: true, weight: "45%" },
    { side: "short", symbol: "ETH", icon: "data:image/png;base64,...", pnl: "+$108", profit: true, weight: "35%" },
    { side: "long", symbol: "BTC", icon: "data:image/png;base64,...", pnl: "-$54", profit: false, weight: "20%" }
  ],
  stats: [
    { label: "Notional", value: "$15,000" },
    { label: "Net Exposure", value: "$1,240" },
    { label: "Hedge", value: "Delta 0.12" }
  ]
});

Rules:

  • pair_trade.legs must contain exactly 2 legs.
  • basket_trade.legs must contain 2-6 legs.
  • Each leg requires side, symbol, and pnl.
  • side must be "long" or "short".
  • stats is optional and capped at 4 items.
  • entry and exit are shown only in the roomier two-leg pair layout.

Perp Cards

Use type: "perp" for perp-style PnL input. Built-in protocol presets fill platform name and icon for hyperliquid and phoenix. platformName and platformIcon override presets.

await generateCard({
  type: "perp",
  protocol: "phoenix",
  pair: "SOL-PERP",
  pnlPercent: 5.93,
  entryPrice: "$88.80",
  exitPrice: "$94.07",
  leverage: 15,
  profit: true
});

Custom perp venue:

await generateCard({
  type: "perp",
  platformName: "Drift",
  platformIcon: "data:image/png;base64,...",
  pair: "SOL-PERP",
  pnlPercent: 12.4,
  entryPrice: "$140.10",
  exitPrice: "$157.48",
  leverage: 10,
  profit: true
});

Phoenix alias:

await generateCard({
  type: "phoenix",
  pair: "SOL-PERP",
  pnlPercent: 5.93,
  entryPrice: "$88.80",
  exitPrice: "$94.07",
  leverage: 15,
  profit: true
});

Prediction Cards

Use type: "prediction" for generic prediction-market cards, or keep using type: "polymarket" for the built-in Polymarket preset.

await generateCard({
  type: "prediction",
  platformName: "Kalshi",
  title: "Fed decision in January",
  outcome: "Yes",
  closesAt: "Closes Jan 16",
  pnlPercent: 40.32,
  entryPrice: "84c",
  exitPrice: "94c",
  profit: true
});

Legacy Compatibility

Existing inputs still work:

await generateCard({
  type: "hyperliquid",
  pair: "SOL -USDC",
  pnlPercent: 40.32,
  entryPrice: "$184.2",
  exitPrice: "$192.5",
  leverage: 20,
  profit: true
});

await generateCard({
  type: "polymarket",
  title: "Fed decision in January",
  outcome: "Yes",
  closesAt: "Closes Jan 16",
  pnlPercent: 40.32,
  entryPrice: "84c",
  exitPrice: "94c",
  profit: true
});

Both legacy types also accept platformName, platformIcon, and tokenIcon overrides.

CLI

suzi-pnl-generator -i card.json -o card.png
suzi-pnl-generator -i card.json --format svg

The CLI accepts the same JSON shapes as the TypeScript API.

JSON Schema

Performance Card

{
  "type": "object",
  "required": ["type", "platform", "subject", "result", "stats"],
  "properties": {
    "type": { "const": "performance" },
    "platform": {
      "type": "object",
      "required": ["name"],
      "properties": {
        "name": { "type": "string", "maxLength": 32 },
        "icon": { "type": "string" }
      }
    },
    "subject": {
      "type": "object",
      "required": ["title"],
      "properties": {
        "title": { "type": "string", "maxLength": 72 },
        "subtitle": { "type": "string", "maxLength": 48 },
        "icon": { "type": "string" },
        "titleMaxLines": { "type": "number", "default": 2 }
      }
    },
    "result": {
      "type": "object",
      "required": ["label", "value", "profit"],
      "properties": {
        "label": { "type": "string", "maxLength": 24 },
        "value": { "type": "string", "maxLength": 24 },
        "profit": { "type": "boolean" }
      }
    },
    "stats": {
      "type": "array",
      "maxItems": 6,
      "items": {
        "type": "object",
        "required": ["label", "value"],
        "properties": {
          "label": { "type": "string", "maxLength": 24 },
          "value": { "type": "string", "maxLength": 32 }
        }
      }
    }
  }
}

Pair and Basket Trade Card

{
  "type": "object",
  "required": ["type", "platform", "title", "result", "legs"],
  "properties": {
    "type": { "enum": ["pair_trade", "basket_trade"] },
    "platform": {
      "type": "object",
      "required": ["name"],
      "properties": {
        "name": { "type": "string", "maxLength": 32 },
        "icon": { "type": "string" }
      }
    },
    "title": { "type": "string", "maxLength": 72 },
    "subtitle": { "type": "string", "maxLength": 48 },
    "result": {
      "type": "object",
      "required": ["label", "value", "profit"],
      "properties": {
        "label": { "type": "string", "maxLength": 24 },
        "value": { "type": "string", "maxLength": 24 },
        "profit": { "type": "boolean" }
      }
    },
    "legs": {
      "type": "array",
      "minItems": 2,
      "maxItems": 6,
      "items": {
        "type": "object",
        "required": ["side", "symbol", "pnl"],
        "properties": {
          "side": { "enum": ["long", "short"] },
          "symbol": { "type": "string", "maxLength": 12 },
          "icon": { "type": "string" },
          "pnl": { "type": "string", "maxLength": 24 },
          "profit": { "type": "boolean" },
          "weight": { "type": "string", "maxLength": 16 },
          "entry": { "type": "string", "maxLength": 24 },
          "exit": { "type": "string", "maxLength": 24 }
        }
      }
    },
    "stats": {
      "type": "array",
      "maxItems": 4,
      "items": {
        "type": "object",
        "required": ["label", "value"],
        "properties": {
          "label": { "type": "string", "maxLength": 24 },
          "value": { "type": "string", "maxLength": 32 }
        }
      }
    }
  }
}

Perp Card

{
  "type": "object",
  "required": ["type", "pair", "pnlPercent", "entryPrice", "exitPrice", "leverage", "profit"],
  "properties": {
    "type": { "enum": ["perp", "phoenix", "hyperliquid"] },
    "protocol": { "enum": ["hyperliquid", "phoenix"] },
    "platformName": { "type": "string" },
    "platformIcon": { "type": "string" },
    "tokenIcon": { "type": "string" },
    "pair": { "type": "string" },
    "pnlPercent": { "type": "number" },
    "entryPrice": { "type": "string" },
    "exitPrice": { "type": "string" },
    "leverage": { "type": "number" },
    "profit": { "type": "boolean" },
    "resultLabel": { "type": "string" },
    "titleMaxLines": { "type": "number" }
  }
}

Design

  • Card: 512x307px, dark gradient background, subtle border
  • Result: green for profit, red for loss
  • Subject/title: Inter
  • Labels: DM Mono
  • Includes Suzi branding and happy/sad mascot based on profit