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

@arkade-os/solver-discovery

v0.2.3

Published

Portable ESM client for discovering solver price feeds from Arkade solver registries (browser / Node / Expo).

Readme

@arkade-os/solver-discovery

A tiny, portable ESM client for the consumer (maker) side of the Arkade Market Discovery Protocol: discover the price feeds solvers advertise, rank markets, convert amounts, and compute the wantAmount for an offer.

  • Runs everywhere — browser, Node, and Expo / React Native. The root entrypoint has zero runtime dependencies; only global fetch is used (and it's injectable).
  • No eval / new Function — validation is hand-rolled, so it works under a strict CSP and on Hermes (React Native), where JSON-Schema engines like Ajv fail.
  • Exact math — prices are BigInt rationals and amounts are BigInt, so no float error ever touches an amount.

Install

npm install @arkade-os/solver-discovery

Quick start

import { discover, listMarkets, bestMarket, quoteOffer } from "@arkade-os/solver-discovery";

// 1. Fetch + merge the registries you follow (plus any pinned local cards).
const { markets, warnings } = await discover({
  registries: ["https://arkade-os.github.io/solver-registry/bitcoin.json"],
});
if (warnings.length) console.warn(warnings);

// 2. List pairs for UI selection, then pick the best market for one pair.
//    `solvable` counts how many markets can pay out each side — don't offer a
//    direction whose receive side is at 0.
console.log(listMarkets(markets).map((p) => `${p.pair} base:${p.solvable.base} quote:${p.solvable.quote}`));
const market = bestMarket(markets, {
  baseId: "btc",
  quoteId: "47004bf4a5fbdb2221f708030528de68ea28f5980044e546b7bb5a352457d1f30000",
  wantSide: "quote", // we give base and receive quote; skips markets that can't pay out quote
});
if (!market) throw new Error("no market solves this side of the pair");

// 3. Quote an offer — fetches the advertised feed and returns a ready plan.
const plan = await quoteOffer(market, { give: "base", giveAmount: "0.01" }); // 0.01 BTC
console.log(`${plan.deposit.display} ${plan.deposit.asset.ticker}`
  + ` -> ${plan.receive.display} ${plan.receive.asset.ticker}`);
// plan.receive.atomic is the wantAmount to request; then createOffer(...) as usual.
if (!plan.limits.withinLimits) console.warn("amount is outside the market's size limits");

Corridor markets

A market side may settle on another rail: base_corridor / quote_corridor name it (arkade — the default — lightning, or onchain), and market identity is the corridor-qualified leg pair, so a Lightning BTC market and an onchain BTC market never collapse into one group. Selection takes corridors the same way it takes ids:

Upgrading from 0.1.x: corridor entries have no feed fields, and 0.1.x quoteOffer throws on them (price_feed was assumed present; its type is now string | undefined). Upgrade before following any registry that lists corridor markets — a registry should not merge its first corridor card until its clients have.

const lnMarket = bestMarket(markets, {
  baseId: "btc",
  quoteId: "btc",
  quoteCorridor: "lightning", // omit corridors and you get spot markets only
  wantSide: "quote",
});

A same-asset corridor market carries no feed — its price is identically 1 and fee_bps plus fee_flat are the whole price. quoteOffer / planOffer handle that transparently (nothing is fetched), but the plan is a pre-quote estimate: the binding amounts come from the solver's RFQ quote, requested via the market's discovery_pubkey over its transports (both present on every corridor entry). The RFQ exchange itself is out of this package's scope.

Amount conversion (Arkade Assets)

Each asset carries a decimals field (8 for BTC and most Arkade assets). Conversion between human and atomic units is exact:

import { toAtomic, fromAtomic } from "@arkade-os/solver-discovery";

toAtomic("1.5", 8);          // => 150000000n
fromAtomic(150000000n, 8);   // => "1.5"
toAtomic("1.123456789", 8);  // throws: more precision than 8 decimals allow

Pricing math always stays in atomic units; quoteOffer() does the human⇄atomic conversion for you using each side's decimals.

Price feed responses

Every market declares both the feed URL and how to read its response:

{
  "price_feed": "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
  "price_feed_schema": { "type": "json", "price_path": "/bitcoin/usd" }
}

price_path is an RFC 6901 JSON Pointer to a JSON number or numeric string. Common examples: Binance ticker price uses /price; a bare JSON number uses the empty pointer "". The client does not scan unknown response shapes.

Pin a local card

Users can pin a solver card directly (a raw card, validated against the card schema), participating in the merge like any registry entry:

const { markets } = await discover({
  registries: ["https://arkade-os.github.io/solver-registry/bitcoin.json"],
  localCards: [{ card: pastedCardJson }],
});

Expo / React Native

Works out of the box — Hermes ships fetch, AbortController, and BigInt. If you target an older runtime without global fetch, inject one:

import { discover } from "@arkade-os/solver-discovery";
await discover({ registries, fetchImpl: myFetch });

React

The optional React entrypoint keeps a two-input quote form synchronized. It fetches the market feed and recalculates the other field whenever the user edits the given or wanted amount:

import { useOfferQuote } from "@arkade-os/solver-discovery/react";

function QuoteForm({ market }) {
  const quote = useOfferQuote(market, { give: "base" });

  if (quote.solvable === false) return <p>This market cannot pay out the side you want.</p>;
  return (
    <>
      <input value={quote.giveAmount} onChange={(e) => quote.setGiveAmount(e.target.value)} />
      <input value={quote.wantAmount} onChange={(e) => quote.setWantAmount(e.target.value)} />
      <button disabled={!quote.plan?.limits.withinLimits}>Create offer</button>
    </>
  );
}

useOfferQuote(market, opts) returns the active input, both display amounts, the latest OfferPlan, loading/error state, and the give/want setters. With give: "base" the give field is the base amount and the want field the quote amount; swap them for give: "quote". quote.solvable says whether the market declares limits for — can pay out — the side received under give (null while no market is selected), and quote.plan?.receive.atomic is the wantAmount to request.

The package does not install React for you: importing @arkade-os/solver-discovery/react requires React in the app, while the root package entrypoint does not import React.

Core API

| Export | Purpose | |---|---| | discover(opts) | Fetch + merge + dedupe + rank markets across registries and local cards. Defaults to network: "bitcoin". Registry failures are isolated. | | fetchIndex(url, opts) | Fetch + validate a single per-network index (never throws). Defaults to network: "bitcoin". | | listMarkets(markets) | List available id pairs, how many solver candidates each pair has, and how many can pay out each side (solvable.base / solvable.quote). | | selectMarkets(markets, {baseId, quoteId, wantSide?, wantAmount?}) / bestMarket(..., {cursor?}) | Filter to one id pair — and optionally to markets that can pay out wantSide, sized by wantAmount on that side — keeping the ranking. cursor: 1 selects the second-ranked market. | | sideLimits(market, side) | The side's { min, max } bounds as exact bigints, or null when the solver cannot pay that side out — disabled (max = "0") or carrying malformed/validation-rejected bounds. The single per-side solvability + size-bound primitive. | | quoteOffer(market, {give, giveAmount \| wantAmount, safetyBps?}) | Fetch the feed and build a full OfferPlan (human in/out). | | planOffer({market, give, giveAmount \| wantAmount, feedValue, safetyBps?}) | Same, from an already-fetched feed value (pure/sync). | | fetchFeedValue(url, schema, opts?) | Fetch a raw feed and extract the numeric price using the market's price_feed_schema. | | deriveAtomicPrice / computeWantAmount | Pure pricing primitives (exact rationals / BigInt). | | toAtomic / fromAtomic / displayPriceString | Decimals-aware conversion. | | validateCard / validateIndex | Dependency-free, eval-free schema validation. |

give: "base" deposits the base asset and receives the quote; give: "quote" is the reverse (priced with 1/P). Pass exactly one of giveAmount or wantAmount: giveAmount fixes the deposit and computes the requested receive amount, while wantAmount fixes the requested receive amount and computes the minimum deposit. Either takes a display string/number, or a bigint to stay in atomic units. safetyBps defaults to 50 — the cushion that absorbs feed movement between funding and fill.

Markets carry size limits for both sides (min|max_base_amount, min|max_quote_amount) as decimal strings of atomic units — exact past 2^53, where JSON numbers silently round — and max = "0" disables a side: the solver cannot pay it out, so a market with zeroed base bounds only serves give: "base". Plans check the received side: plan.limits.min === null means the market cannot pay that side at all, and plan.limits.withinLimits says whether the received amount sits inside [min, max].

Roadmap

Chained (multi-hop) offers — not yet supported: bestMarket/quoteOffer match direct (baseId, quoteId) pairs only. Planned: treat markets as directed edges over canonical asset ids and route through intermediates (BTC → USDT → USDC) via findRoutes / planRoute / quoteRoute, ranking routes by the compounded net multiplier ∏(1 − (fee_bps + safety_bps)/10000) and checking size limits per hop at plan time. Note the protocol caveat: each hop executes as a separate Arkade offer, so a chained route is not atomic — plans are indicative and routing will be opt-in, never a silent fallback inside quoteOffer(). Full spec and API design: #1.

Develop

pnpm install
pnpm test        # node --test, dependency-free, runs the .ts directly
pnpm typecheck
pnpm build       # emits dist/*.js + *.d.ts

Release

From the repo root, publish a patch, minor, or major release:

npm run release:client -- patch

The last argument must be patch, minor, or major. The script must be run from master with a clean git tree, runs the package tests/typecheck/build, bumps packages/discovery-client/package.json with npm version <level> --no-git-tag-version, publishes to npm, then creates a release commit and a solver-discovery-vX.Y.Z tag, and pushes both master and the tag to origin.

You can also run the same workflow from this package directory:

npm run release -- minor

Before running it, make sure you are logged in to npm with publish rights for the @arkade-os scope.