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

@baliola/smart-account-sdk

v0.3.1

Published

Client-side SDK for Baliola's Smart Account (ERC-4337) stack on MAC

Readme

@baliola/smart-account-sdk

Client-side TypeScript SDK for Baliola's ERC-4337 v0.7 stack on MAC chain. A thin viem extension that handles UserOperation packing, signing, bundler RPC, and Gas Allowance Pool (paymaster) wiring. Pick a chain by name, plug in your API key, send transactions.

const account = await toSmartAccount({ owner, chain: "macTestnet" });
const client = await createSmartAccountClient({
  account,
  chain: "macTestnet",
  apiKey: BALIOLA_KEY,
});

const userOpHash = await client.writeContract({
  address: nft,
  abi: nftAbi,
  functionName: "mint",
  args: [1n],
});

const receipt = await client.waitForUserOperationReceipt({ userOpHash });

Install

bun add @baliola/smart-account-sdk viem
# or: npm install / pnpm add

viem (^2.46) is a peer dependency. Install it alongside.

Quick Start

import { privateKeyToAccount } from "viem/accounts";
import {
  createSmartAccountClient,
  toSmartAccount,
} from "@baliola/smart-account-sdk";

const owner = privateKeyToAccount(process.env.OWNER_KEY as `0x${string}`);

const account = await toSmartAccount({
  owner,
  chain: "macTestnet",
});

// createSmartAccountClient is async. It validates the API key against
// baliola-auth before returning.
const client = await createSmartAccountClient({
  account,
  chain: "macTestnet",
  apiKey: process.env.BALIOLA_KEY!,
});

const userOpHash = await client.writeContract({
  address: "0xYourContract",
  abi: yourAbi,
  functionName: "doSomething",
});

const receipt = await client.waitForUserOperationReceipt({ userOpHash });
console.log("included in tx:", receipt.txHash);

Chains

The SDK is opinionated about MAC. Pick a chain by name and everything else is preconfigured: chain RPC, bundler RPC, and the auth service URL.

| chain | Network | |---|---| | "macTestnet" | MAC Testnet | | "macMainnet" | MAC Mainnet |

import { macTestnet, macMainnet, resolveChain, MAC_CHAINS } from "@baliola/smart-account-sdk";

resolveChain("macTestnet");       // viem Chain object
Object.keys(MAC_CHAINS);          // ["macTestnet", "macMainnet"]

API Key Validation

createSmartAccountClient validates the API key once at construction time. On success, the key info (including quota) is exposed on client.apiKey. On rejection it throws ApiKeyInvalidError; on transport failure it throws BaliolaAuthUnreachableError.

try {
  const client = await createSmartAccountClient({
    account,
    chain: "macTestnet",
    apiKey: BALIOLA_KEY,
  });

  client.apiKey.id;                       // apiKeyId (uuid)
  client.apiKey.accountId;                // baliola account uuid
  client.apiKey.module;                   // "smart-account"
  client.apiKey.quota.callsRemaining;     // remaining calls in the period
  client.apiKey.quota.periodResetsAt;     // ISO 8601 timestamp
} catch (err) {
  if (err instanceof ApiKeyInvalidError) {
    switch (err.reason) {
      case "revoked":
      case "expired":
      case "not_found":
      case "wrong_module":
      case "module_inactive":
      case "origin_not_allowed":
        // bad key; user needs to fix it
        break;
      case "quota_exceeded":
      case "rate_limited":
        // back off using err.retryAfterSeconds
        break;
    }
  } else if (err instanceof BaliolaAuthUnreachableError) {
    // baliola-auth down or network issue; safe to retry
  }
}

Each construction consumes one quota call. Cache the client; don't rebuild it on every request.

To point at a custom auth host (for staging or dev), pass authUrl:

await createSmartAccountClient({
  account,
  chain: "macTestnet",
  apiKey: BALIOLA_KEY,
  authUrl: "https://your-auth-host",
});

createSmartAccountClient options

await createSmartAccountClient({
  account,                              // required, from toSmartAccount
  chain,                                // required, "macTestnet" | "macMainnet"
  apiKey,                               // required
  authUrl?,                             // override the chain's default auth host
  paymasterAddress?,                    // override the chain's default Gas Allowance Pool
  bundlerUrl?,                          // override the default bundler URL
  bundlerTransport?,                    // full viem Transport escape hatch
  transport?,                           // chain RPC transport (defaults to chain's built-in RPC)
});

Resolution precedence:

  • Auth URL: authUrl then the default for the chain.
  • Chain RPC: transport then http() of the chain default.
  • Bundler RPC: bundlerTransport then bundlerUrl then the chain default.
  • Paymaster: paymasterAddress then the chain's registered Gas Allowance Pool, otherwise throw PaymasterConfigError.

toSmartAccount options

toSmartAccount({
  owner,                                // required, viem Account or "0x"-prefixed 32-byte private key
  chain,                                // required, "macTestnet" | "macMainnet"
  salt?,                                // CREATE2 salt for counterfactual address (default 0n)
  nonceKey?,                            // EntryPoint nonce key (default 0n)
  transport?,                           // chain RPC transport (defaults to chain's built-in RPC)
  publicClient?,                        // share a publicClient across calls (default: built internally)
});

Returned ISmartAccount is counterfactual until the first UserOperation is sent. The factory call is injected automatically.

writeContract, the single write verb

Same shape for everything: a typed call, a raw call, or a batch.

Typed contract call

const userOpHash = await client.writeContract({
  address: counter,
  abi: counterAbi,
  functionName: "increment",
  args: [],
  value: 0n,                            // optional
});

Raw call (ETH transfer or pre-encoded data)

// Pure native transfer
await client.writeContract({ to: recipient, value: 1n });

// Pre-encoded calldata
await client.writeContract({ to: target, value: 0n, data: "0xabcd..." });

Batch with an array

Mixed typed and raw entries are allowed in one batch. The whole batch is one UserOperation submitted via SimpleAccount.executeBatch.

const userOpHash = await client.writeContract([
  { address: usdc, abi: erc20Abi, functionName: "approve", args: [router, amount] },
  { address: router, abi: routerAbi, functionName: "swap", args: [...] },
  { to: refundAddr, value: 1n },        // raw entry alongside typed ones
]);

Overrides

A second optional argument lets you override UserOperation-level fields (gas, fee, paymaster, nonce). Most callers never touch this.

await client.writeContract(input, {
  preVerificationGas: 200_000n,
  maxFeePerGas: 5_000_000_000n,
});

Receipts

// One-shot; returns null if not yet included
const maybe = await client.getUserOperationReceipt({ userOpHash });

// Poll until included or timeout
const receipt = await client.waitForUserOperationReceipt({
  userOpHash,
  timeout: 60_000,
  pollingInterval: 1_000,
  signal: abortSignal,                  // optional AbortSignal
});

receipt.userOpHash;     // ERC-4337 hash
receipt.txHash;         // on-chain handleOps tx hash
receipt.blockNumber;
receipt.success;
receipt.actualGasUsed;
receipt.actualGasCost;
receipt.logs;           // pre-sliced to this UserOp

Watching events

Subscribe to EntryPoint.UserOperationEvent filtered by sender or a specific userOpHash. The SDK decodes each match into typed fields.

const unwatch = client.watchUserOperations({
  // Defaults to your account's address when omitted
  sender: account.address,
  onUserOp(event) {
    console.log(event.userOpHash, event.success, event.actualGasCost);
  },
  onError(err) { console.error(err); },
});

unwatch();                              // stop the subscription

Paymaster (Gas Allowance Pool)

Every UserOperation is sponsored by a paymaster. The default is the Gas Allowance Pool (GAP) registered for the chain, no configuration required.

// Default: uses the chain's GAP
await createSmartAccountClient({ account, chain: "macTestnet", apiKey });

// Override: route through a different paymaster contract
await createSmartAccountClient({
  account, chain: "macTestnet", apiKey,
  paymasterAddress: "0x...",
});

paymasterAddress is validated at construction. Invalid or zero addresses throw PaymasterConfigError. Self-funded mode is not supported.

Errors

Every SDK error extends SmartAccountSDKError. The library is opinionated about messages: err.message is always written for an end user (short, polite, and safe to surface directly in product UI). Technical detail lives on:

  • err.code: internal tag or AA code (e.g. "API_KEY_INVALID", "AA24")
  • err.reason: typed enum on errors that have one (ApiKeyInvalidError, PaymasterConfigError)
  • err.detail: optional technical sentence for developer logs
  • err.cause: the underlying transport or library error

Discriminate with instanceof, not message matching.

SmartAccountSDKError                    // root
├─ UserOperationReceiptTimeoutError
├─ PaymasterConfigError                 // dev-config error, carries `reason`
├─ ApiKeyInvalidError                   // baliola-auth rejected the key (carries `reason`)
├─ BaliolaAuthUnreachableError          // network or 5xx talking to baliola-auth
├─ BundlerRpcError
│   ├─ InvalidUserOperationError        // bundler rejected the params
│   ├─ UserOperationRejectedError       // bundler dropped on submit
│   └─ BundlerUnreachableError          // network or timeout
└─ UserOperationRevertError             // AA-code reverts
    ├─ AccountFactoryError              // AA1x
    ├─ AccountValidationError           // AA2x: signature, nonce, prefund
    ├─ PaymasterValidationError         // AA3x: paymaster rejection
    └─ BundleFrameError                 // AA9x

Surfacing errors

You can pipe err.message straight into a toast or banner. It's already user-safe:

try {
  await client.writeContract({ /* ... */ });
} catch (err) {
  toast(err instanceof Error ? err.message : "Something went wrong.");
  console.error(err); // full technical detail still on err.detail / err.cause
}

Default user messages

| Error | err.message | |---|---| | ApiKeyInvalidError (reason=revoked) | "This API key has been revoked. Please generate a new one." | | ApiKeyInvalidError (reason=expired) | "This API key has expired. Please generate a new one." | | ApiKeyInvalidError (reason=quota_exceeded) | "You've hit the request limit. Please try again later." | | ApiKeyInvalidError (reason=rate_limited) | "Too many requests. Please slow down and try again." | | ApiKeyInvalidError (reason=not_found) | "Invalid API key. Please check your credentials." | | BaliolaAuthUnreachableError | "Authentication service is temporarily unavailable. Please try again in a moment." | | BundlerUnreachableError | "Couldn't reach the network. Please check your connection and try again." | | UserOperationReceiptTimeoutError | "Your transaction is taking longer than expected. Please check the block explorer or try again." | | PaymasterValidationError (AA3x) | "Gas sponsorship was declined for this transaction. Please try again later." | | AccountValidationError (AA2x) | "Your transaction couldn't be authorized. Please try again." | | AccountFactoryError (AA1x) | "We couldn't set up your smart account. Please try again." | | BundleFrameError (AA9x) | "Something went wrong submitting your transaction. Please try again." | | InvalidUserOperationError | "Your transaction was rejected before submission. Please try again." | | UserOperationRejectedError | "Your transaction was rejected by the network. Please try again." | | PaymasterConfigError | "Gas sponsorship isn't available for this network. Please contact support." |

Tailoring the copy

If you need different tone, branding, or i18n, switch on the typed fields and write your own copy. Every error carries enough metadata to do so:

function copy(err: unknown): string {
  if (err instanceof ApiKeyInvalidError) {
    switch (err.reason) {
      case "quota_exceeded":
        return err.retryAfterSeconds
          ? `Limit hit, try again in ${err.retryAfterSeconds}s.`
          : "Daily limit reached.";
      case "revoked": return "Your access has been revoked.";
      // ...
    }
  }
  if (err instanceof Error) return err.message; // sensible default
  return "Something went wrong.";
}

Subpath imports

import { createSmartAccountClient } from "@baliola/smart-account-sdk";
import { toSmartAccount } from "@baliola/smart-account-sdk/accounts";
import { writeContract } from "@baliola/smart-account-sdk/actions";
import { validateApiKey, DEFAULT_AUTH_URLS } from "@baliola/smart-account-sdk/auth";
import { macTestnet, MAC_CHAINS } from "@baliola/smart-account-sdk/chains";
import { DEFAULT_BUNDLER_URLS } from "@baliola/smart-account-sdk/clients";
import { SmartAccountSDKError } from "@baliola/smart-account-sdk/errors";
import type { UserOperationReceipt } from "@baliola/smart-account-sdk/types";

License

Proprietary. Copyright 2026 Baliola. All rights reserved. See LICENSE.