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

@arcus-xyz/arcus-spot-sdk

v1.0.2

Published

TypeScript SDK for the Arcus spot router server.

Readme

@arcus-xyz/arcus-spot-sdk

TypeScript SDK for the Arcus spot router server. It wraps the router HTTP API and provides a viem-first signing flow for firm quotes routed through SwapShell — Arcus RFQ, Rialto, and LI.FI venues on Robinhood mainnet (4663) and testnet (46630).

viem is the only runtime dependency (a peer dependency); the SDK ships no other runtime deps.

Install

Published to the public npm registry — no registry configuration or token required:

npm install @arcus-xyz/arcus-spot-sdk viem
# or: bun add @arcus-xyz/arcus-spot-sdk viem

Hosted routers

Public router deployments are available — no local setup or API key is needed to fetch quotes:

| Environment | Base URL | Chain ID | Venues | | ----------------- | ------------------------------------------ | -------- | ------------------- | | Robinhood mainnet | https://router.spot.arcus.xyz/v1 | 4663 | arcus, rialto, lifi | | Robinhood testnet | https://router.spot.testnet.arcus.xyz/v1 | 46630 | arcus |

Verify either with the unversioned health endpoint, e.g. curl https://router.spot.arcus.xyz/health → {"ok":true,"chainId":4663,...}. Self-hosted routers (e.g. http://localhost:8787/v1) work the same way — every example below accepts either base URL.

Usage

Robinhood testnet (Arcus)

Point the client at a router serving chain 46630 — the hosted testnet router above, or a locally-run router (http://localhost:8787/v1). The wallet must be on chain 46630. Default mock trade pair: mUSDG → mTSLA (fetch token addresses via getTokenList() below).

import {
  ROBINHOOD_TESTNET_CHAIN_ID,
  SpotRouterClient,
  buildArcusSellTokenPermitIfNeeded,
  erc20ApproveAbi,
  MAX_UINT256,
  PermitUnsupportedError,
  signQuote,
} from "@arcus-xyz/arcus-spot-sdk";
import { createPublicClient, createWalletClient, custom, http } from "viem";

const client = new SpotRouterClient({ baseUrl: "https://router.spot.testnet.arcus.xyz/v1" });
const publicClient = createPublicClient({
  chain: { id: ROBINHOOD_TESTNET_CHAIN_ID, name: "robinhood-testnet" },
  transport: http("https://rpc.testnet.chain.robinhood.com"),
});
const walletClient = createWalletClient({
  account: "0xYourWallet",
  transport: custom(window.ethereum),
});

const quotes = await client.getQuote({
  chainId: ROBINHOOD_TESTNET_CHAIN_ID,
  sellToken: "0xf64780eAE9CFe162EF38f5224459a014a1007cd5", // mUSDG
  buyToken: "0x01206fc62E2e88df71cE4b591e93Bb203383482B", // mTSLA
  sellAmount: "10000000",
  taker: "0xYourWallet",
  slippageBps: 50,
});

const quote = quotes.all.find((q) => q.venue === "arcus");
if (!quote || quote.venue !== "arcus") throw new Error("No Arcus quote");

// Optional: one-time EIP-2612 permit when sellToken→Permit2 allowance is missing
// (returns undefined when the allowance is already set — no signature prompt).
// Non-EIP-2612 tokens can't permit: PermitUnsupportedError describes the one-time
// on-chain approve to Permit2 to send instead; retry after it mines.
let permit;
try {
  permit = await buildArcusSellTokenPermitIfNeeded({ quote, publicClient, walletClient });
} catch (error) {
  if (!(error instanceof PermitUnsupportedError)) throw error;
  const hash = await walletClient.writeContract({
    address: error.token,
    abi: erc20ApproveAbi,
    functionName: "approve",
    args: [error.spender, MAX_UINT256], // spender is always the canonical Permit2
    chain: null,
  });
  await publicClient.waitForTransactionReceipt({ hash });
  // Retry re-reads the allowance on-chain instead of trusting the receipt.
  permit = await buildArcusSellTokenPermitIfNeeded({ quote, publicClient, walletClient });
}

const signed = await signQuote(quote, walletClient, {
  permits: permit ? [permit] : undefined,
});
const submitResponse = await client.submitSignedQuote(signed);
if (submitResponse.venue === "arcus") {
  console.log(submitResponse.txHash, submitResponse.status, submitResponse.orderId);
}

PermitUnsupportedError also exposes sellAmount and currentAllowance — USDT-style tokens revert on a nonzero→nonzero approve, so send approve(0) first when currentAllowance is nonzero. Only contract-shaped failures (missing/reverting nonces()) classify a token as non-EIP-2612; transport errors are rethrown so a flaky RPC never downgrades a gasless flow to a gas-costing tx. The rialto and lifi builders share the same behavior.

The SDK accepts the versioned API base URL, for example http://localhost:8787/v1, and calls endpoints like /quote, /price, /submit, /status, and /tokens relative to it. health() remains unversioned at /health.

Every firm quote includes fees, a normalized route-fee array with amount in atoms and token as the fee token address. Venues with no reported fee return an empty array; Bebop gas/native fees may include amountUsd to show the USD value of the fees.

Example quote.fees from a firm quote:

[
  {
    "amount": "1500",
    "token": "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
    "type": "volume"
  },
  {
    "amount": "16826",
    "token": "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
    "type": "gas"
  }
]

Chain deployments and token list

Onchain addresses are bundled per chain (published with ABIs in arcus-xyz/spot-contracts-abis):

import {
  getChainDeployments,
  getSwapShellAddress,
  ROBINHOOD_TESTNET_CHAIN_ID,
  ROBINHOOD_TESTNET_DEPLOYMENTS,
  SpotRouterClient,
} from "@arcus-xyz/arcus-spot-sdk";

const deployments = getChainDeployments(ROBINHOOD_TESTNET_CHAIN_ID);
// deployments.swapShell, .arcusSettlement,
// .arcusWrappedTokenFactory, .arcusWrappedTokenBeacon, ...

getSwapShellAddress(46630); // => ROBINHOOD_TESTNET_DEPLOYMENTS.swapShell

const client = new SpotRouterClient({ baseUrl: "https://router.spot.arcus.xyz/v1" });
const tokens = await client.getTokenList();
// [{ chainId, symbol, name, address, decimals, source, wrappedTokenAddress? }, ...]

| Chain | ID | Key exports | | ----------------- | ----- | ------------------------------------------------------- | | Arbitrum One | 42161 | ARBITRUM_SWAP_SHELL | | Robinhood mainnet | 4663 | ROBINHOOD_MAINNET_DEPLOYMENTS (no public RPC default) | | Robinhood testnet | 46630 | ROBINHOOD_TESTNET_DEPLOYMENTS |

All chains are also available via getChainDeployments(chainId) and CHAIN_DEPLOYMENTS_BY_ID.

Use client.getTokenList() to fetch the router's supported tokens for the configured chain.

Use getSwapShellTradeHistory() with chainId: 46630 (or pass swapShell explicitly) to read SwapExecuted logs for a taker.

Wrapped token address prediction

predictWrappedToken derives the canonical wrapped representation of an underlying token (e.g. mTSLA → wrapped mTSLA, or mainnet WEEK → wWEEK) entirely offline — a pure CREATE2 derivation mirroring WrappedTokenFactory.predictWrappedToken on-chain. No RPC calls are made. The address is well-defined whether or not the wrapped token has been deployed yet (the escrow/factory deploys it on the first fill), so treat the result as the canonical address, not proof of deployment.

predictWrappedTokenForChain is the convenience form: it reads the configured WrappedTokenFactory and beacon for a chain and throws if that chain has no wrapped-token deployment.

import { predictWrappedTokenForChain, ROBINHOOD_TESTNET_CHAIN_ID } from "@arcus-xyz/arcus-spot-sdk";

const wrappedTsla = predictWrappedTokenForChain({
  chainId: ROBINHOOD_TESTNET_CHAIN_ID,
  underlying: "0x01206fc62E2e88df71cE4b591e93Bb203383482B", // mTSLA
});

Use the lower-level predictWrappedToken when you need to pass factory/beacon addresses explicitly (e.g. a deployment not bundled in the SDK). Robinhood mainnet (4663) WEEK → wWEEK:

import { ROBINHOOD_MAINNET_DEPLOYMENTS, predictWrappedToken } from "@arcus-xyz/arcus-spot-sdk";

const wWeek = predictWrappedToken({
  wrappedTokenFactory: ROBINHOOD_MAINNET_DEPLOYMENTS.arcusWrappedTokenFactory!,
  wrappedTokenBeacon: ROBINHOOD_MAINNET_DEPLOYMENTS.arcusWrappedTokenBeacon!,
  underlying: "0xc93a8c440CEa26D7445dF01729f193b27965099f",
});
// => 0x4B17e556568bB02709a50cA67db7F4DBD46E3d17

Pass the WrappedTokenFactory proxy and WrappedToken beacon for your deployment. Wrong inputs yield a deterministic but incorrect address — the function does not validate chain wiring.

Local demo

Demo UI showing aggregate quote selection, signing, submission, and status panels

The demo ships presets for the hosted routers, so no router setup is required. Start it with:

bun install
bun run demo

Open http://127.0.0.1:5173, connect an injected wallet, fetch firm quotes, choose one, sign, submit, and poll status.

For Robinhood testnet: select the testnet router preset (sets chain ID to 46630 and fills the testnet SwapShell address), connect a wallet on RH testnet, and trade mUSDG → mTSLA with the arcus venue. The local preset (http://localhost:8787/v1) remains for self-hosted routers.

Develop

bun install        # install dependencies
bun run build      # emit dist/ (tsc)
bun run typecheck  # typecheck src + demo
bun run test       # run the bun test suite
bun run demo       # run the browser demo

Releasing

Publishing is automated by .github/workflows/release.yml using npm trusted publishing (OIDC) — no long-lived npm token is stored in the repo. The workflow runs on every published GitHub Release (and can be triggered manually from the Actions tab). It typechecks, tests, builds dist/, and publishes to the public npm registry with provenance attached automatically.

To cut a release:

  1. Bump version in package.json and merge it to main.
  2. Create a GitHub Release whose tag matches that version. Tags v0.2.0, sdk-v0.2.0, and [email protected] are all accepted and resolve to 0.2.0; the workflow verifies the resolved version matches package.json.

Re-publishing an existing version fails by design, so always bump the version first.

One-time setup for OIDC publishing

OIDC trusted publishing cannot perform the first publish of a brand-new package (npm requires the package to exist before its trusted publisher can be configured). So the very first release is a manual step:

  1. Initial manual publish (once), from a maintainer's machine with an npm login that can publish to the @arcus-xyz scope:

    bun install && bun run build
    npm publish --access public
  2. Configure the trusted publisher on npmjs.com: open the package → Settings → Trusted Publisher → GitHub Actions, and set:

    • Organization or user: arcus-xyz
    • Repository: arcus-spot-sdk
    • Workflow filename: release.yml
    • Environment: leave blank (unless you add a GitHub environment gate)

After that, every subsequent release publishes automatically via OIDC — no token, no manual step.