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

@operon/sdk

v0.3.0

Published

Operon SDK - request placements from the Operon ad network

Readme

@operon/sdk

Operon's TypeScript SDK. Connect any agent to the ad network.

Install

npm install @operon/sdk

Works in both ESM and CommonJS Node.js projects. Node 18+.

Quickstart

The SDK ships dual ESM + CJS builds, so the same package works in both project styles. Pick the snippet that matches your project's "type" field in package.json (default for npm init -y is CommonJS). Both snippets run as printed.

ESM ("type": "module", or .mjs):

import { initOperon } from "@operon/sdk";

// Defaults to https://api.operon.so - pass { url } to override.
const operon = initOperon();

// Your agent supplies the user's message; this stands in for it.
const userQuery = "cheapest way to swap 2 ETH to USDC?";

const result = await operon.getPlacement(userQuery, {
  placement_context: "user asked about swapping ETH",
});

if (result.decision === "filled") {
  console.log("Recommendation:", result.placement?.service);
}

// _meta carries sandbox/quota info. It can be absent - access with ?.
console.log(result._meta?.is_sandbox);

CommonJS (default for npm init -y, no "type": "module"):

const { initOperon } = require("@operon/sdk");

// CommonJS has no top-level await - keep the calls inside an async function.
async function main() {
  const operon = initOperon(); // defaults to https://api.operon.so

  const userQuery = "cheapest way to swap 2 ETH to USDC?";

  const result = await operon.getPlacement(userQuery, {
    placement_context: "user asked about swapping ETH",
  });

  if (result.decision === "filled") {
    console.log("Recommendation:", result.placement?.service);
  }

  console.log(result._meta?.is_sandbox);
}

main();

TypeScript: full types ship with the package and resolve under "strict": true with any modern moduleResolution (NodeNext, bundler). Run the ESM snippet directly with npx tsx index.ts - no tsconfig tweaks needed.

Once you've made a call, confirm the SDK is wired up correctly:

npx @operon/sdk test     # fire one sandbox getPlacement
npx @operon/sdk status   # show SDK state (UUID, source, registration)

Both commands contact api.operon.so, and each counts as 1 call against your sandbox quota - use them ad hoc, not in a health-check loop. Every command supports --help (e.g. npx @operon/sdk register --help).

Register for production demand

After integrating, register to bump your sandbox quota and join the early-access list for production demand:

npx @operon/sdk register

register is interactive. Scripted use works too - pipe one answer per line (printf '[email protected]\nelizaos\nDeFi agent\n2\n' | npx @operon/sdk register); if input ends before every prompt is answered, the CLI exits 1 with an error instead of silently registering nothing. The prompts are email, framework name, agent description, and a monthly call volume tier:

Operon Publisher Registration

Registering raises your sandbox quota from 100 to 1000 placements/hr
and puts you on the early-access list for production demand.

We'll only email you about quota changes and demand availability.

We'll use this to follow up about production demand pool access. No marketing spam.
Email: [email protected]

Help us prioritize which framework SDKs to ship next.
Framework (e.g. elizaos): elizaos

Help us understand who's building and what for.
What does your agent do? DeFi research agent

Used to assign appropriate quotas and prioritize early-access invites.
Expected monthly calls:
  1) <1K       (testing / hobbyist)
  2) 1K-10K    (small production agent) [default]
  3) 10K-100K  (mid-tier production)
  4) 100K-1M   (high-volume)
  5) >1M       (commercial)
Choice [2]:

On success, the CLI echoes back your registration and the new quota:

Done. You're registered.

  Email:                 [email protected]
  Framework:             elizaos
  Agent:                 DeFi research agent
  Expected volume:       1K-10K
  Source:                (not set, that's fine)
  Status:                registered
  Sandbox quota:         1000 placements/hr (was 100)

Next steps:
  - Try a placement (see https://operon.so/developers)
  - Watch your impressions log
  - Ready for production? Email [email protected] for your API key
    (a one-line change: add apiKey to initOperon)

API

initOperon(options)

| Option | Type | Description | |--------|------|-------------| | url | string? | Base URL of the Operon API. Defaults to the OPERON_URL env var when set, then https://api.operon.so. Must be non-empty when provided. | | publisherName | string? | Display name for your agent. Defaults to operon-sdk. Max 200 characters. | | apiKey | string? | Optional Bearer token. Only needed for authenticated publishers. Sandbox usage does not require a key. | | timeoutMs | number? | Request timeout. Defaults to 10000. | | source | string? | Marketplace/skill attribution tag ([A-Za-z0-9._-], max 64). First-touch wins: persisted on first valid call, ignored on later inits with a different value. | | onRetryable | (err: OperonRetryableError) => void? | Observability hook fired when the server returns 429/503 + Retry-After, before the error is thrown. Use for metrics, Sentry, alerting, etc. Errors thrown by the callback are swallowed. Note that the SDK does not await the callback's return value - async callbacks should not throw; wrap async work in a try/catch internally. | | maxRetryAfterMs | number? | Cap on Retry-After-derived backoff hints (ms). Defaults to 60_000. Invalid values (0, negative, NaN, Infinity) fall back to the default. |

getPlacement(query, context)

query is the user's natural-language input. The server caps query (and every other string field it receives: category, asset, amount, intent, sentiment, plus publisherName) at 200 characters, and actions must be an array of strings. The SDK enforces all of this client-side: an empty query or an invalid field throws instantly with an actionable message, before any network call - no quota burned, circuit breaker unaffected. Truncate long user messages before calling.

context must include placement_context (a one-sentence description of what the user is doing, 1-1000 characters - the SDK enforces the cap because the server silently drops longer values). Optional fields: asset, intent, category, sentiment, amount, actions. intent is a hint; when omitted, Operon classifies it from the query. Recognized values include swap, buy, stake, bridge, compare, optimize, rebalance.

Returns a placement response - see the next section. Inspect _meta?.is_sandbox to know if you got mock demand.

Response fields

A filled response looks like this (sandbox example, trimmed):

{
  "decision": "filled",
  "reason": "...",
  "placement": {
    "sponsored": true,
    "service": "Helix Exchange",
    "category": "DeFi",
    "description": "Multi-chain swap aggregator routing across 40+ DEXs.",
    "bidPrice": 195,
    "creative": "Helix Exchange routes your swap across 40+ DEXs for best execution.",
    "clickUrl": "https://api.operon.so/c/imp_2cd29aefdb418a66",
    "disclosure": null
  },
  "auction": { "candidates": 3, "eligible": 3, "winner": "Helix Exchange" },
  "_meta": { "is_sandbox": true, "message": "...", "client_uuid": "..." }
}

The fields a publisher acts on:

| Field | Meaning | |-------|---------| | decision | "filled" or "blocked". Blocked responses carry a reason string and no placement - they are a normal outcome, not an error. | | placement.service | Advertiser/service name to mention in your response. | | placement.creative | Ready-to-merge description text, when the advertiser provides one (null otherwise). | | placement.clickUrl | Tracking link. Route the placement's link through this URL - linking the advertiser directly breaks click attribution, and unattributed clicks don't pay. | | placement.sponsored | Always true on placements. Render your sponsorship disclosure from this, e.g. a "via operon" note in your response format. | | placement.disclosure | Extra category disclosure text (e.g. age restrictions). null means none is required beyond your standard sponsorship note. Show it when present. | | placement.bidPrice | Winning bid for the impression, in basis points of 1 USDC: divide by 10,000 for dollars (195 = about $0.02). | | auction | Diagnostic detail about the auction round (candidates considered, winner). Useful for logging; treat it as informational, not stable API. | | _meta | Sandbox and quota info: is_sandbox, message, client_uuid. Optional in the types because responses without a client UUID omit it - always access with ?.. |

Configuration

| Env var | Purpose | |---------|---------| | OPERON_CLIENT_ID | Override the persisted UUID. Use in CI or ephemeral environments without writeable home directories. Also disables source persistence so CI runs don't pollute developer-machine attribution. | | OPERON_SOURCE | Override the attribution tag at request time. Doesn't write to disk. Use in CI/containers to test attribution behavior without affecting persisted state. | | OPERON_URL | Default API URL for SDK calls and every CLI command when url isn't passed. Defaults to https://api.operon.so. The SDK requires https: for non-loopback values (a mutated env var would otherwise redirect requests, including your apiKey, to another host) and warns once when the override is active. Production publishers using apiKey should pass url explicitly. |

Sandbox vs production

Until production demand is live, all placements are sandbox. Check _meta?.is_sandbox in responses. The request path, response shape, and quota behavior are the same as production - only the demand pool is mock.

The first 100 calls per UUID return a register prompt. Past 100, the message upgrades to a quota nudge. Past 1000, it includes a personal note pointing to [email protected]. Registered users get 10x quota and a different message.

Errors

Everything getPlacement can throw, and what to do about it:

| Condition | What you catch | Notes | |-----------|----------------|-------| | Client-side validation: empty query, missing or over-long placement_context (1000 max), a field over 200 chars, bad actions | Error, message starts @operon/sdk: | Thrown before any network call - no quota burned, never counts toward the breaker. Fix the call site. | | Throttled: rate limit (60 req/min) or hourly quota, HTTP 429 + Retry-After; or sandbox ceiling, HTTP 503 + Retry-After | OperonRetryableError with retryAfterMs and status | Wait and retry. Does not count toward the breaker. | | Server rejected the request | Error, message starts Operon <status>: (400 validation, 429/503 without Retry-After, 500) | The server's reason is included. Counts toward the circuit breaker. | | Timeout (timeoutMs, default 10s) | The platform's DOMException named TimeoutError | Counts toward the breaker. | | DNS or connection failure | The platform's TypeError: fetch failed | The underlying code (e.g. ENOTFOUND) is on err.cause. Counts toward the breaker. | | Circuit open | Error, message contains circuit breaker open | Thrown synchronously, no request made. Auto-recovers within 30s. |

Cross-build caveat: if your app mixes the ESM and CJS builds of this package (for example an ESM app plus a CJS wrapper library), instanceof OperonRetryableError can fail across the boundary because each build has its own class identity. In mixed setups, check err.name === "OperonRetryableError" instead.

Circuit breaker

Five failures within 30 seconds opens the circuit. While open, getPlacement throws synchronously without making a request. The window slides, so the circuit auto-closes once 30 seconds pass without new failures.

What counts as a failure: network errors, timeouts, and non-2xx responses - including 400s, so validate field lengths client-side to keep bad input from opening the circuit. What doesn't count: 429s and 503s carrying Retry-After (OperonRetryableError), client-side validation errors, and breaker-open rejections themselves.

Handling throttling (429 / 503)

When the server throttles - HTTP 429 for the per-key rate limit (60 requests/min) or the hourly sandbox quota (100/hr unregistered, 1000/hr registered), or HTTP 503 for the sandbox-lane global ceiling - and includes a Retry-After header, the SDK throws OperonRetryableError carrying retryAfterMs and status. These do NOT count toward the circuit breaker.

Note: retryAfterMs is capped at maxRetryAfterMs (default 60s), but the hourly quota sends Retry-After: 3600 - treat retryAfterMs as a minimum wait, not the full window. A 429 quota error means the hour's budget is spent.

import { initOperon, OperonRetryableError } from "@operon/sdk";

const op = initOperon();

async function placementOrNull(query: string, ctx: { placement_context: string }) {
  try {
    return await op.getPlacement(query, ctx);
  } catch (err) {
    if (err instanceof OperonRetryableError) {
      // Server asked us to wait; e.g. skip this placement and continue
      console.log(`throttled, retry after ${err.retryAfterMs}ms`);
      return null;
    }
    throw err;
  }
}

Observability hooks and backoff tuning

Use onRetryable to pipe throttle events into your metrics or alerting stack. The hook fires before the throw - your try/catch still runs normally. Use maxRetryAfterMs to override the default 60s cap on Retry-After hints (useful when your loop budget is tighter):

import { initOperon, OperonRetryableError } from "@operon/sdk";

const op = initOperon({
  // Fired when 503 + Retry-After comes back. Doesn't gate the throw -
  // your try/catch still runs. Use this for metrics, alerts, etc.
  onRetryable: (err) => {
    metrics.increment("operon.retryable", { retryAfterMs: err.retryAfterMs });
  },
  // Override the default 60s cap on retry hints.
  maxRetryAfterMs: 30_000,
});