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

@osero/client

v0.8.0

Published

The official TypeScript SDK for minting USDS and sUSDS across supported chains

Readme

@osero/client

The official TypeScript SDK for minting USDS and sUSDS on every chain where Sky / Spark runs a PSM. One API, wallet integrations for viem, ethers v6, and Privy server wallets, five chains out of the box.


What it does

Given a wallet holding USDC, @osero/client builds and sends the right sequence of transactions to land USDS or sUSDS in any address you name, no matter which chain you are on:

| Chain | Chain ID | Route | | ------------ | -------: | --------------------------------------------------- | | Ethereum | 1 | Spark UsdsPsmWrapper (+ ERC-4626 sUSDS deposit) | | OP Mainnet | 10 | Spark PSM3 | | Unichain | 130 | Spark PSM3 | | Base | 8453 | Spark PSM3 | | Arbitrum One | 42161 | Spark PSM3 |

The SDK figures out which contract to talk to, reads the live fee (tin / tout) or swap quote, assembles the approval and swap transactions, and hands you back a wallet-agnostic ExecutionPlan that any adapter can broadcast.

It also exposes preview helpers for every exact-in flow so callers can quote the expected output amount before building or sending a plan.

The package also includes @osero/client/api, a small HTTP client for the hosted Osero API. It can fetch supported API assets, build API swap quotes, convert those quotes into SDK execution plans, and poll bridge status for cross-chain quotes.

Install

pnpm add @osero/client viem
# (optional) for the ethers adapter:
pnpm add ethers
# (optional) for the Privy server-wallet adapter:
pnpm add @privy-io/node

viem is a required peer dependency — the SDK uses it to encode calldata and to build public clients internally. ethers and @privy-io/node are optional; install them only if you use @osero/client/ethers or @osero/client/privy.

Quick start

With viem

import { OseroClient } from '@osero/client';
import { mintSUsds } from '@osero/client/actions';
import { sendWith } from '@osero/client/viem';
import { createWalletClient, http, parseUnits } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';

const privateKey = process.env.PRIVATE_KEY as `0x${string}` | undefined;
if (!privateKey) throw new Error('Set PRIVATE_KEY before sending transactions');

const client = OseroClient.create({
  transports: {
    8453: http('https://mainnet.base.org'),
  },
});

const wallet = createWalletClient({
  account: privateKeyToAccount(privateKey),
  chain: base,
  transport: http('https://mainnet.base.org'),
});

const result = await mintSUsds(client, {
  chainId: 8453,
  amount: parseUnits('100', 6), // 100 USDC
  sender: wallet.account.address,
}).andThen(sendWith(wallet));

if (result.isErr()) {
  console.error(result.error.name, result.error.message);
  return;
}

console.log('sUSDS minted in tx', result.value.txHash);

With ethers v6

import { OseroClient } from '@osero/client';
import { mintUsds } from '@osero/client/actions';
import { sendWith } from '@osero/client/ethers';
import { JsonRpcProvider, Wallet, parseUnits } from 'ethers';

const privateKey = process.env.PRIVATE_KEY;
if (!privateKey) throw new Error('Set PRIVATE_KEY before sending transactions');

const provider = new JsonRpcProvider('https://arb1.arbitrum.io/rpc');
const signer = new Wallet(privateKey, provider);

const client = OseroClient.create();

const result = await mintUsds(client, {
  chainId: 42161,
  amount: parseUnits('1000', 6),
  sender: await signer.getAddress(),
}).andThen(sendWith(signer));

if (result.isOk()) {
  console.log('USDS minted in tx', result.value.txHash);
}

With Privy server wallets

import { PrivyClient } from '@privy-io/node';
import { OseroClient } from '@osero/client';
import { mintUsds } from '@osero/client/actions';
import { sendWith, type PrivyWallet } from '@osero/client/privy';
import { parseUnits } from 'viem';

const privy = new PrivyClient({
  appId: process.env.PRIVY_APP_ID!,
  appSecret: process.env.PRIVY_APP_SECRET!,
});

const wallet = {
  id: process.env.PRIVY_WALLET_ID!,
  address: process.env.PRIVY_WALLET_ADDRESS as `0x${string}`,
  authorizationContext: {
    authorization_private_keys: [process.env.PRIVY_AUTHORIZATION_PRIVATE_KEY!],
  },
} satisfies PrivyWallet;
const client = OseroClient.create();

const result = await mintUsds(client, {
  chainId: 8453,
  amount: parseUnits('100', 6),
  sender: wallet.address,
}).andThen(sendWith(privy, wallet));

if (result.isOk()) {
  console.log('USDS minted in tx', result.value.txHash);
}

authorization_private_keys must be the base64-encoded PKCS8 private key registered for your Privy app. Omit authorizationContext only if your Privy wallet configuration does not require request authorization.

Actions

Every action returns a ResultAsync<ExecutionPlan, …> that you pipe into sendWith (from @osero/client/viem, @osero/client/ethers, or @osero/client/privy) to execute.

| Action | Direction | Mainnet shape | L2 shape | | ------------- | ------------ | ------------------ | ------------------- | | mintUsds | USDC → USDS | approve + sellGem | approve + PSM3 swap | | mintSUsds | USDC → sUSDS | four-tx two-phase | approve + PSM3 swap | | redeemUsds | USDS → USDC | approve + buyGem | approve + PSM3 swap | | redeemSUsds | sUSDS → USDC | three-tx two-phase | approve + PSM3 swap |

Matching preview helpers return the quoted output amount as a raw bigint wrapped in ResultAsync. They only take chainId and amount because they do not build a sender-specific execution plan:

| Preview helper | Quotes | Input decimals | | -------------------- | ------------ | -------------: | | previewMintUsds | USDC → USDS | 6 | | previewMintSUsds | USDC → sUSDS | 6 | | previewRedeemUsds | USDS → USDC | 18 | | previewRedeemSUsds | sUSDS → USDC | 18 |

import { previewMintSUsds } from '@osero/client/actions';
import { parseUnits } from 'viem';

const quote = await previewMintSUsds(client, {
  chainId: 8453,
  amount: parseUnits('100', 6),
});

if (quote.isOk()) {
  console.log('expected sUSDS out:', quote.value);
}

Request shape

type Request = {
  chainId: number; // one of the supported chain IDs
  amount: bigint; // input amount in the input token's native decimals
  sender: `0x${string}`; // the wallet that pays the input
  receiver?: `0x${string}`; // default = sender
  slippageBps?: number; // default = client.config.defaultSlippageBps (5)
  referralCode?: bigint; // emitted on L2 PSM3 swaps and mainnet sUSDS deposits
};

Balance helpers

@osero/client also exposes read-only helpers for canonical token balances so callers can stick with the SDK's chain registry and public client wiring instead of dropping down to raw ERC-20 reads.

import {
  getSUsdsBalance,
  getTokenBalance,
  getTokenBalances,
  getUsdcBalance,
  getUsdsBalance,
} from '@osero/client';

Choose the helper that matches the job:

  • getTokenBalance(client, { chainId, account, token }) reads one of the three canonical symbols: USDC, USDS, or sUSDS
  • getTokenBalances(client, { chainId, account }) returns all three balances in one keyed result object
  • getUsdcBalance, getUsdsBalance, and getSUsdsBalance keep the common single-token cases terse
const result = await getTokenBalances(client, {
  chainId: 8453,
  account: wallet.account.address,
});

if (result.isOk()) {
  console.log(result.value.USDC);
  console.log(result.value.USDS);
  console.log(result.value.sUSDS);
}

All balance helpers return raw bigint values and surface UnsupportedChainError or UnexpectedError through the same ResultAsync model used by the action builders.

Osero API client

Use @osero/client/api for hosted API quotes instead of building the route locally. The client sends x-api-key, validates the public API key shape before making requests, and returns typed ResultAsync values.

import { flattenExecutionPlan } from '@osero/client';
import { OseroApiClient } from '@osero/client/api';
import { parseUnits } from 'viem';

const api = OseroApiClient.create({
  apiKey: process.env.OSERO_API_KEY!,
  // Defaults to https://api.osero.org/v1/
  baseUrl: process.env.OSERO_API_BASE_URL,
});

const quote = await api.getSwapQuote({
  fromAddress: '0x1111111111111111111111111111111111111111',
  fromAssetId: 'base:usdc',
  toAssetId: 'ethereum:susds',
  amount: parseUnits('1', 6),
  slippage: '0.5',
  referralCode: 3000,
});

if (quote.isErr()) {
  console.error(quote.error.name, quote.error.message);
  return;
}

console.log('sUSDS out:', quote.value.quote.amountOut?.formatted);
console.log(flattenExecutionPlan(quote.value.executionPlan));

To execute the hosted quote through a wallet, hand off the attached executionPlan to the same viem, ethers, or Privy adapter used by local action builders:

import { sendWith } from '@osero/client/viem';

const result = await api
  .getSwapQuote(request)
  .map((quote) => quote.executionPlan)
  .andThen(sendWith(wallet));

Common calls:

  • getSupportedAssets() returns the public asset IDs accepted by the quote endpoint.
  • getSwapQuote(request) builds approval and execution transactions for supported counter asset ↔ sUSDS routes.
  • getSwapStatus({ txHash, sourceChainId, bridgeProtocol }) checks a bridge status request returned by a cross-chain quote.
  • getSwapStatusForQuote(quote, txHash) is a convenience wrapper that pulls sourceChainId and bridgeProtocol off quote.bridge.statusRequest for you. Same-chain quotes have no bridge to track and return a ValidationError.

The client-level API key can be overridden per request:

await api.getSupportedAssets({ apiKey: 'osero_partner-key' });

Quote referralCode is optional. When provided, it must be an integer from 3000 to 3999 and overrides the referral code attached to the authenticated API key for that quote.

Error handling

@osero/client uses neverthrow for functional error handling. Every action returns a ResultAsync. Chain them with .andThen, then call .isOk() / .isErr() at the top level:

const result = await mintUsds(client, request).andThen(sendWith(wallet));

if (result.isErr()) {
  switch (result.error.name) {
    case 'CancelError':
      // user rejected the wallet prompt
      break;
    case 'ValidationError':
      // bad input (amount <= 0, etc.)
      break;
    case 'UnsupportedChainError':
      // chainId is not in SUPPORTED_CHAIN_IDS
      break;
    case 'TransactionError':
      // tx was broadcast but reverted — inspect .txHash / .link
      break;
    case 'ApiRequestError':
      // hosted API returned a non-2xx response — inspect .statusCode / .body
      break;
    case 'SigningError':
    case 'UnexpectedError':
      // RPC failure, bad signature, etc. — .cause has the original
      break;
  }
  return;
}

Every error class extends OseroError, which itself extends the built-in Error, so instanceof OseroError is the broadest catch.

Configuration

import { OseroClient } from '@osero/client';
import { http } from 'viem';

const client = OseroClient.create({
  // Override the default public transports for every chain you care
  // about — strongly recommended for production.
  transports: {
    1: http('https://eth.llamarpc.com'),
    10: http('https://mainnet.optimism.io'),
    130: http('https://mainnet.unichain.org'),
    8453: http('https://mainnet.base.org'),
    42161: http('https://arb1.arbitrum.io/rpc'),
  },

  // Default slippage (in bps) used by any action that doesn't pass
  // its own `slippageBps`. Defaults to 5 (= 0.05%).
  defaultSlippageBps: 10,
});

Set confirmation waits on the viem, ethers, or Privy adapter when broadcasting:

const result = await mintUsds(client, request).andThen(sendWith(wallet, { confirmations: 2 }));

The Privy adapter can also receive transports for receipt polling, using the same chain-keyed shape as OseroClient.create({ transports }).

For server-side retries with Privy, pass caller-managed idempotency keys. The SDK does not derive keys from transaction contents because two unrelated operations can produce identical transaction data. Store a fresh key with the operation you are about to execute and reuse that same key only when retrying that operation:

const result = await mintUsds(client, request).andThen(
  sendWith(privy, wallet, {
    idempotencyKeys: [storedIdempotencyKey],
  }),
);

Plans with approvals or multiple phases send more than one transaction. In those cases, provide one key per transaction in the same order returned by flattenExecutionPlan(plan). The adapter fails before broadcasting anything if the number of keys does not match the number of transactions, so no transaction is ever sent without its retry protection.

The execution plan model

Every action returns an ExecutionPlan, a wallet-agnostic description of the transactions that need to happen. The adapter (sendWith) is the only piece that touches a real wallet. This gives you three things for free:

  1. Dry-run — inspect the plan without signing anything:

    import { flattenExecutionPlan } from '@osero/client';
    
    const result = await mintSUsds(client, request);
    if (result.isOk()) {
      for (const tx of flattenExecutionPlan(result.value)) {
        console.log(tx.operation, tx.to, tx.data);
      }
    }
  2. Portability — pass the same plan to a viem wallet, an ethers signer, a Privy server wallet, a custom batching relayer, or an account-abstraction bundler. Only sendWith needs to change.

  3. Testability — actions are pure functions over an OseroClient; unit-testing them without a live chain is a matter of injecting a mock PublicClient.

Plans come in three shapes:

  • TransactionRequest — a single pre-encoded tx.
  • Erc20ApprovalRequired — one or more approvals gating a single main tx (e.g. every L2 mint / redeem).
  • MultiStepExecution — an ordered list of the above (e.g. mainnet sUSDS mint, which is USDC → USDS → sUSDS in two phases).

Supported chains & contracts

All addresses live in the source tree in src/lib/addresses.ts and src/lib/tokens.ts, and are re-exported from the package root:

import { SUPPORTED_CHAIN_IDS, CHAINS, PSM_ADDRESSES, getToken } from '@osero/client';

console.log(PSM_ADDRESSES[8453].psm); // Spark PSM3 on Base
console.log(getToken(1, 'sUSDS').address); // sUSDS on mainnet

Building & testing

This package is part of the Osero SDK Nx workspace. From the repo root:

pnpm nx build @osero/client      # tsc → dist/
pnpm nx typecheck @osero/client  # strict tsc --noEmit
pnpm nx test @osero/client       # vitest run

License

MIT