Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Nonce management

Stacks' /v2/accounts returns only the confirmed nonce — it ignores the mempool. Broadcasting several transactions from one account before the first confirms makes them reuse the same nonce, so every one after the first is rejected (ConflictingNonceInMempool). The usual workaround is tracking nonces by hand.

Attach a nonce manager and the SDK hands out sequential nonces across rapid broadcasts:

import { createWalletClient, http, createNonceManager } from "@secondlayer/stacks";
import { mainnet } from "@secondlayer/stacks/chains";
import { privateKeyToAccount } from "@secondlayer/stacks/accounts";
 
const client = createWalletClient({
  chain: mainnet,
  transport: http(),                  // any node — no Secondlayer dependency
  account: privateKeyToAccount(process.env.KEY!),
  nonceManager: createNonceManager(), // jsonRpcSource + in-memory store
});
 
// 20 back-to-back transfers → nonces n, n+1, …, n+19 — no collisions
await Promise.all(
  recipients.map((to) => client.transferStx({ to, amount: 1000n })),
);

Passing an explicit nonce always bypasses the manager. The defaults are node-agnostic and in-memory, with zero external dependencies. A send that fails before the node accepts it (fee estimate outage, FeeTooLow, transport error) hands its nonce back, so the next send reuses it instead of leaving a gap the mempool cannot chain past. Custom stores opt in by implementing release(key, nonce); the bundled memory, Redis and Postgres stores already do.

Multiple processes / smart wallets

The in-memory store is single-process. Backends that sign from one key across multiple workers (smart-wallet-as-a-service) need a shared, durable store. The reservation is atomic in the datastore, so it doubles as the cross-process lock and survives restarts:

import { createNonceManager, redisStore } from "@secondlayer/stacks";
 
const nonceManager = createNonceManager({
  store: redisStore({ redis: new Bun.RedisClient(process.env.REDIS_URL!) }),
});

postgresStore({ sql }) works the same way. Bring your own client — no global Bun reference, so the store stays runtime-agnostic.

Mempool-aware sources (optional)

By default the floor is the node's confirmed nonce. To make it mempool-aware, and to auto-fill the freed nonce of a dropped transaction, swap the source. indexSource reads your Secondlayer instance's mempool through the client transport (retries, timeout and Authorization: Bearer included), hiroNonceSource reads Hiro's, or bring your own pending feed with mempoolAwareSource:

import {
  createNonceManager,
  indexSource,
  startNonceReconciler,
} from "@secondlayer/stacks";
 
// indexSource({ baseUrl?, apiKey? }) | hiroNonceSource({ baseUrl }) | mempoolAwareSource({ getPending })
// baseUrl defaults to the transport URL; pass it when the transport is not your instance.
// A missing URL, a Hiro transport, or a host without /v1/index rejects instead of
// silently falling back to the confirmed nonce; a 5xx or timeout still degrades.
const source = indexSource();
const nonceManager = createNonceManager({ source });
 
// Optional: periodically heal silently-dropped txs (run in ONE process)
const reconciler = startNonceReconciler(nonceManager, {
  client,
  addresses: [account.address],
  source,
});

Everything here is opt-in. With no source/store, the manager depends only on your node.