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

@openperps/keeper

v1.4.1

Published

Core-only self-host keeper for OpenPerps: oracle/funding cranks and liquidation, across many markets.

Readme

@openperps/keeper

npm license

The core-only self-host keeper for OpenPerps. A keeper is part of the risk system, not just a price cron: it pushes oracle/funding updates on-chain and submits liquidations across many markets.

v1 scope is intentionally small. It does not include analytics, candles, billing, a hosted tenant registry, a trade feed API, or an SLA system.

Use

import { Connection, Keypair } from "@solana/web3.js";
import { createStaticPriceProvider } from "@openperps/sdk";
import { runKeeper, type KeeperMarket } from "@openperps/keeper";

const connection = new Connection(process.env.OPENPERPS_RPC!, "confirmed");
const authority = Keypair.fromSecretKey(/* your oracle authority key */);

const markets: KeeperMarket[] = [
  { config: sampleMarketConfig, maxAccrualDtSlots: 1000, maxPriceMoveBpsPerSlot: 10 },
];

await runKeeper(
  { connection, authority, priceProvider: createStaticPriceProvider(100_000_000n) },
  markets,
  { intervalMs: 60_000 },
);

For a relayer market with no Pyth feed (custom SPL, memecoins), use createLivePriceProvider from @openperps/sdk: it reads the token's USD price off DexScreener then Jupiter, scales it to the market's price decimals, and holds the last good price when both are momentarily down. Or bring your own PriceProvider (Birdeye, Pyth, a pool read, Geyser, your own oracle) instead of the static demo provider.

import { createLivePriceProvider } from "@openperps/sdk";

const priceProvider = createLivePriceProvider(); // DexScreener -> Jupiter -> last-known

Run as a relayer daemon

runKeeper is the loop; runRelayer is the deployable process around it. It defaults the price source to the live provider, derives each market's catch-up bounds from its risk tier (keeperMarketFromConfig), serves /health, and runs until aborted, so a MANUAL/relayer market gets a live mark pushed on-chain without you writing any of that:

import { Connection, Keypair } from "@solana/web3.js";
import { runRelayer, keeperMarketFromConfig } from "@openperps/keeper";

const controller = new AbortController();
await runRelayer({
  connection: new Connection(process.env.OPENPERPS_RPC!, "confirmed"),
  authority: Keypair.fromSecretKey(/* oracle authority key */),
  markets: marketConfigs.map((c) => keeperMarketFromConfig(c)),
  healthServer: { port: 18810 }, // GET /health -> 200 healthy, 503 stale/failing
  signal: controller.signal,
});

Or run the bundled CLI (openperps-relayer), configured by environment:

OPENPERPS_RPC=https://api.devnet.solana.com \
OPENPERPS_KEEPER_KEYPAIR=./keeper.json \
OPENPERPS_MARKETS=./markets.json \
npx openperps-relayer
# optional: OPENPERPS_INTERVAL_MS (60000), OPENPERPS_HEALTH_PORT (18810), OPENPERPS_HEALTH_HOST (0.0.0.0)

OPENPERPS_MARKETS is a market-config json (or array) as produced by the SDK's createPerpMarket; each is validated on load. The CLI installs SIGINT/SIGTERM handlers for a clean shutdown.

Per-market crank cadence

keeperMarketFromConfig also sets each market's push cadence. Both tiers default to a fast ~2s cadence: this is not just freshness, it keeps the on-chain mark close to the live price so there is little stale-mark gap for latency arbitrage to exploit against the House (a mark that lags a fast pump lets an informed trader long into a known move). The loop ticks at the fastest market's cadence and throttles each market to its own pushIntervalMs; set a slower, cheaper cadence for a calm market via keeper.expectedCrankIntervalMs in the config, or the pushIntervalMs override on keeperMarketFromConfig.

Authority

For AccrueAsset, the keeper authority keypair must match the market's oracle authority. If it does not, the program rejects the oracle/funding update.

By default that authority is the program's global relayer constant. A market authority can instead rotate it per market with the SDK's setOracleAuthorityIx (an [ORACLE_SEED, market] PDA). For such a market, set useOracleAuthorityPda: true on its KeeperMarket so the keeper passes the PDA to AccrueAsset, and run the keeper with the keypair you set as that market's oracle authority.

Freshness

The keeper respects the engine's per-slot price-move bound and max_accrual_dt_slots freshness window. When a market has fallen behind, buildAccrualInstructions bursts catch-up accruals (capped per cycle) so the asset is current before risk-increasing trades are attempted. Each KeeperMarket declares both bounds via maxAccrualDtSlots and maxPriceMoveBpsPerSlot: a large price jump is split into steps that each stay within the per-slot move budget (oldPrice * maxPriceMoveBpsPerSlot * dt / 10000), so no single AccrueAsset is rejected for moving the price too far too fast. See ../../docs/keeper-freshness.md.

Liquidation

discoverLiquidatable scans the program's portfolio accounts and returns the candidates for a market: every account with an open position in the asset, minus the House. liquidatePortfolio submits a permissionless Liquidate, simulating first so a healthy account (which the engine rejects) costs no transaction fee. scanLiquidations runs the whole candidate set the same way and returns the signatures that landed, so the keeper finds and clears underwater accounts on its own. For a very large deployment, front discovery with an indexer instead of a full getProgramAccounts scan.

Monitoring

Create a KeeperHealth and pass it on deps.health; the runner records, per market, the last crank, how many slots behind the chain it was, whether it is stale (behind its freshness window), the last error, and a failure streak, plus running totals. Read it live and serve it from your own endpoint:

import { createKeeperHealth, summarizeHealth, runKeeper } from "@openperps/keeper";

const health = createKeeperHealth();
void runKeeper({ connection, authority, priceProvider, health }, markets, { intervalMs: 60_000 });

// in your HTTP handler:
//   res.json({ ...summarizeHealth(health), totals: health.totals });

summarizeHealth returns { healthy, staleMarkets, failingMarkets } for a one-glance /health check. The pure helpers marketBehind and isMarketStale are available if you want to compute freshness yourself.

License

Apache-2.0.