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

@ripar/sdk

v0.1.0

Published

Ship a paid HTTP endpoint on Algorand. Define a handler, set a price, and every call settles in USDC over x402.

Readme

@ripar/sdk

Ship a paid HTTP endpoint on Algorand. Write a handler, set a price, and every call settles in USDC over x402.

Not on npm yet. npm install @ripar/sdk will 404 — version 0.1.0 has not been published. Until it is, install from the repo:

git clone https://github.com/nickthelegend/ripar-sdk && cd ripar-sdk
npm install && npm run build
npm link                       # then `npm link @ripar/sdk` in your project

Every import below is the real published surface, so nothing changes when it ships.

Start from a template

ripar init my-agent --template basic    # one echo endpoint, the smallest paid thing
ripar init my-agent --template llm      # prompt completion, priced per token budget
ripar init my-agent --template oracle   # price quotes signed with an Algorand key
cd my-agent && npm install && ripar dev

| Command | What it does | | --- | --- | | ripar init <name> | Scaffold from a template | | ripar dev [entry] | Run an agent locally — no build step, node strips the types | | ripar quote <url> | Read a price. Free, no wallet, no funds | | ripar call <url> | Pay for and invoke an endpoint | | ripar manifest <url> | Print an agent's published manifest | | ripar doctor | Check node, facilitator, network and payout address before you advertise a URL |

ripar call reads the wallet from RIPAR_MNEMONIC. There is deliberately no --mnemonic flag: it would land in your shell history and in ps.

The whole thing

import { defineAgent, defineEndpoint, serve } from "@ripar/sdk";

const summarize = defineEndpoint({
  name: "summarize",
  price: "$0.01",
  input: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
  handler: ({ body }) => ({ summary: body.text.slice(0, 280) }),
});

await serve(defineAgent({
  name: "Text Tools",
  handle: "text-tools",
  description: "Small, cheap text utilities priced per call.",
  payTo: process.env.RIPAR_PAY_TO!,   // settlement lands here directly
  endpoints: [summarize],
}));

There is no payment code in that file, and that is the point. serve() puts x402 in front, so an unpaid request never reaches your handler.

Three things that happen before payment

await serve(agent, {
  rateLimit:   { perMinute: 60, per: "payer" },   // 429 + Retry-After
  idempotency: { windowMs: 10 * 60_000 },         // honour Idempotency-Key
});

The order is rate limit → idempotency → input validation → payment → handler, and the position is the feature. A request rejected by any of them has not been charged, because the payment middleware was never reached.

  • Input validation — the body is checked against the same input schema discovery publishes. A missing field is a 400 naming the field, not a settled payment for a call that could never have worked.

  • Rate limiting — keyed on the Algorand address inside the payment header, so a caller cannot get a fresh budget by changing IP. Off unless you pass rateLimit, in-process (two replicas allow two windows), and in per: "payer" mode it does not limit unpaid traffic at all — an unpaid request has no payer to charge the hit to. Note the header is not signature-checked before the limiter reads it, so the payer key is a claim rather than proof; see the KNOWN GAP note in src/identity.ts before relying on it to protect a caller.

  • Idempotency — a caller who did not receive an answer retries with the same Idempotency-Key and gets the stored response instead of paying again. A 402 is never cached, or the paid retry would be answered with the quote it was replying to; nor is a 5xx, because that response refunded the caller.

    The guarantee is exactly "a completed 2xx is replayed, not re-run". It is not "a dropped connection can never be charged twice": a request that settled and then lost its socket before writing a response releases its claim, and the retry does reach the payment middleware and settle again. In-process, so the store is per-replica too — two instances keep two stores.

Pricing per request

price can be a function. It runs before the 402 is written, so the caller signs for the amount their own request implies:

defineEndpoint({
  name: "complete",
  price: ({ body }) => `$${(0.002 + 0.0004 * Math.ceil(body.maxTokens / 100)).toFixed(4)}`,
  priceHint: "$0.0024–$0.0100 (by maxTokens)",   // what discovery shows
  handler: …,
});

It has to be cheap (every unpaid probe pays its cost) and deterministic for a given body (the caller quotes, signs, and sends again — a price that moved in between rejects a payment they built correctly).

Calling one

import { RiparClient } from "@ripar/sdk";

const ripar = new RiparClient({
  mnemonic: process.env.WALLET_MNEMONIC,
  maxPrice: "$0.05",    // refuse anything dearer, whatever the quote says
  maxPerDay: "$5",      // and stop entirely once the day's budget is gone
  retry: { attempts: 3 },
});

const res = await ripar.call("https://api.example.com/summarize", { text: "…" });
res.data;            // { summary: "…" }
res.payment?.txId;   // the Algorand transaction that settled it
res.payment?.usd;    // what it cost, converted from atomic units

quote() needs no wallet and no funds, so price discovery is free.

Retries use exponential backoff with full jitter, on 5xx and transport failures only. A 4xx is never retried: a 402 is the handshake, a 400 will be wrong again, and repeating either can pay twice for the same mistake. Both caps are checked against the quote before anything is signed, and a quote the client cannot read is refused rather than paid blind.

Watching it

| Route | Unpaid | What it is | | --- | --- | --- | | GET /health | yes | Platform probes | | GET /.well-known/ripar.json | yes | Discovery: price, input schema, asset | | GET /metrics | yes | Prometheus: requests by endpoint+status, in-flight gauge, duration histogram, total settled | | GET /_ripar/runs | yes | The last N calls: id, endpoint, status, ms, txId |

/_ripar/runs is a ring buffer, not a log. It carries no request bodies and no payer addresses — it is world-readable, so it holds only what an operator needs to answer "is it working and did it settle".

Announcing it

import { registerWithBazaar } from "@ripar/sdk";

const result = await registerWithBazaar("https://my-agent.fly.dev/.well-known/ripar.json");
if (!result.ok) console.warn("not listed:", result.error);

It never throws. Being listed brings callers; not being listed costs reach and nothing else — an agent that crashed on boot because a registry was down would take a working paid endpoint offline over a directory entry.

Deploy it anywhere

Dockerfile plus ready configs in deploy/ for Railway, Render, Fly.io and Heroku. /health is deliberately unpaid so a platform probe does not read a healthy agent as down.

SIGTERM drains: the server stops accepting work, lets in-flight calls finish, and only then exits, with a hard timeout so an orchestrator's SIGKILL never lands mid-write. A paid call killed in flight has already settled — the caller paid and got a dropped connection.

docker build -t my-agent .
docker run -p 4021:4021 -e RIPAR_PAY_TO=YOUR_ALGORAND_ADDRESS my-agent

Tests

npm test

Unit tests cover the validation that stops expensive mistakes. Integration tests run a real server and assert an unpaid call gets a 402 and the handler never runs. test/mainnet.test.ts hits the live facilitator and checks the quote carries USDC 31566704.

| Suite | What it pins down | | --- | --- | | test/sdk.test.ts | Definition-time validation, the 402 gate, the manifest | | test/guards.test.ts | 400 before payment, per-request pricing, 429, replay-not-recharge | | test/observability.test.ts | Header decoding, metrics, the run buffer, draining | | test/client.test.ts | Retry policy, spend caps, bazaar tolerance | | test/cli.test.ts | Every command, every template, the oracle's signature | | test/mainnet.test.ts | Live facilitator negotiation on MainNet |

CI splits those two groups on purpose. .github/workflows/ci.yml runs typecheck, build and the offline suite as the gate, and runs test/mainnet.test.ts as a second, advisory job — it talks to GoPlausible over the public internet, and a third-party outage should be loud without being a merge blocker. It still goes red, and red still means go and look.

To reproduce the split locally:

npx vitest run --exclude '**/node_modules/**' --exclude '**/dist/**' --exclude 'test/mainnet.test.ts'
npx vitest run test/mainnet.test.ts

The two default excludes are restated because --exclude on the CLI replaces the config's list instead of adding to it, and dropping them lets vitest wander into node_modules.

Gotchas worth knowing

The CAIP-2 network id has two forms. @x402/avm exports a truncated genesis hash, while facilitators advertise the full one. Hardcoding either breaks against the other, with an error that reads like an outage. This SDK resolves the id from the facilitator's /supported at boot.

The x402 headers are base64, and the amounts are atomic. PAYMENT-REQUIRED and X-PAYMENT-RESPONSE carry base64 JSON — JSON.parse on the raw value throws, and code that swallows that throw reads every quote as "unknown", which is how a price cap ends up never firing while looking like it is on. Inside, amount is in the asset's atomic units: 102000 is $0.102 of USDC, not $102000. src/headers.ts decodes both and refuses to guess the decimals of an asset it does not know.

See ARCHITECTURE.md.

MIT.