Skip to main content
Version scope, checked September 14, 2026: the consistency refactor is merged in the SDK repository, and published as 4.9.6-consistency.16 under the consistency tag. npm latest is still 4.9.5. The controller examples in these docs target that exact prerelease. A merge does not publish a package or upgrade a client. Check current registry metadata before selecting a version.

What changed

Account, Deposit, Withdraw, Supply, Pay and Lock now acquire balances and evaluate asset eligibility through shared SDK inventory and policy. Supply, Pay, Lock and custom action presentations use useSpiceSupply for planning and execution. useSpiceLock and the existing useSpiceAction are thin adapters. Deposit and rollup Withdraw retain their distinct execution semantics; Account wallet withdrawal uses the shared fee-only action path. The benefit is one implementation of shared behavior across stock and custom views. A correction to fees or inventory no longer needs a separate client implementation. The tradeoff is a larger set of consumers to validate when shared behavior changes, plus migration work for integrations that used to execute destination transactions themselves. This refactor is not a promise of a smaller bundle or faster execution on every route.

Integration checklist

  1. Record the current app pin and resolved SDK/core versions. Compare them with the intended published package, its exports and declarations. A higher version on another prerelease branch does not prove it contains this refactor.
  2. Keep the existing wallet provider stack and import styles.css once. Configure SDK rpcOverrides for every source, conversion and destination chain the app can exercise. wagmi transports alone do not configure the SDK. See Configuration.
  3. Keep the host UI. Use the stock component when it fits, or the controlled action hook. Replace host-owned balance timers, fee estimation, funding orchestration and execution state with the shared SDK owners.
  4. Audit the callbacks and contract ownership described below. Preserve approved asset restrictions, beneficiary semantics and application behavior; do not remove a restriction merely to pass a test.
  5. Validate the exact candidate in each affected app, then update its exact pin and lockfile through the app’s release process. Document pending migrations instead of claiming universal compatibility.

Callback and ownership migration

The three execution overrides above now produce integration errors. Keeping their export/type names does not make them functional compatibility paths. This change does not remove every onDepositExecute or onWithdrawExecute contract; inspect those distinct flows before changing them. External mode selects the external source wallet, funds the embedded wallet, and then uses the same downstream action executor as embedded mode. The source wallet is not necessarily the destination contract caller. Use BuildActionCallsContext.executorAddress and beneficiary deliberately. For example, a contract offering stakeFor(beneficiary, amount) can preserve a user’s position owner; a contract assigning ownership only to msg.sender needs a separate integration decision. Verify NFT receiver callbacks as well as token balances. Do not delete an override and silently change who owns the resulting position.

Same-chain destination assets do not automatically bypass the SDK

The behavior below describes published consistency candidate .16. The unpublished SDK PR #104 restores automatic direct execution inside the shared controller for External mode and a matching wallet token/chain. It uses the same host action definition, needs no embedded signer, and charges no Spicenet fee. Network gas is paid in the external wallet, with a gas reserve for native Max. Calls are sequential, with receipt tracking and recovery of unfinished calls; success callbacks are still awaited. Other tokens/chains, Embedded mode, fee-only/proceeds-funded actions and zero-amount payments retain their existing behavior. Existing funded actions are reconciled on their original path. Do not assume .16 includes this restoration; adopt and verify an exact new candidate after publication.
In the refactored Supply/Pay/Lock controller, selecting the destination token on its own chain avoids conversion, but does not select external-wallet destination execution. For example, external USDC on Base funding a Base USDC action still takes the external-to-embedded funding stage and shared executor. The controller disables its former direct-external action path. This was previously SDK-owned behavior. Before the refactor, Supply and Pay detected an external source that matched the destination token and chain, then called the SDK’s executeDirectExternalAction. Lock had the equivalent executeDirectExternalLock branch. The host supplied protocol calls; the SDK selected the direct path and submitted those calls with the external signer, without first funding the embedded wallet. See the pre-refactor Supply executor and Lock executor. Implementing a separate native action in the host is therefore a workaround for a removed SDK capability, not an unchanged client responsibility. If a host already has such an action, audit its routing before claiming it was preserved. The External mode toggle does not provide that bypass in .16. The unpublished restoration above keeps this choice in the shared SDK controller, so clients continue to supply one action definition.

Amounts, fees and Max

Preserve raw integer amounts and the decimals of the token each amount belongs to. Use the controller’s accepted preview and canExecute; do not recompute readiness from a nonempty input. Use resolveMaxAmount(asset) for Max. A null result means Max is unavailable, not permission to use the full balance. An accepted plan binds the source identity/cap, beneficiary, destination minimum, route/calls, fee bounds, expiry and provider/policy configuration. A stale or changed plan must be reviewed again. Some changing-fee failures are intentional: execution must fit the accepted bounds. The refactor does not guarantee zero quote retries. Deposit gas and rollup withdrawal rules remain distinct from these action fees. Account wallet withdrawal uses a fee-only Supply action; do not apply a blanket “withdrawals never need a quote” rule to that path.

Inventory and refresh

Use useSpiceInventory for custom holdings and the action controller’s asset list for action selection. assetPolicy is shared internal logic, not a new provider prop. Clients configure allowedTokens, chain scope, RPCs and venues.
  • Keep owner, wallet/rollup location, chain and token/receipt identity. Use assetPositionId(asset) for row keys and selection matching; equal symbols do not mean equal positions.
  • Complete raw inventory retains dust and unpriced positions. Default visibility is separate: discoveries outside configured tokens need a finite positive price and at least $0.01. Account also hides configured priced dust; configured tokens retain the missing-price exception.
  • Preserve displayed data during background refresh. Confirmed zero replaces the old balance; stale/failed balance reads cannot authorize spending. A price-only failure is different from a balance failure. Do not turn refreshing into an empty list or an initial loading screen.
  • Extra assets can supply metadata and otherwise-undiscovered positions. They cannot overwrite an SDK-acquired balance, including zero. Apply the same client policy across views.
Inventory is scoped by owner, API, RPC and fetcher identity. The underlying quote/execution transports still have global state; simultaneous differently configured providers are not supported by this isolation alone.

Completion and recovery

Render the controller’s status until required credit/delivery and destination receipts complete. Await application updates in the success callback and make them safe to retry. A failed application update can be retried with retryFinalization() without resubmitting the transaction. Use the existing pending-action journal for uncertain results. Preserve the original action inputs, beneficiary and configuration when resuming. Keep lock duration and other host-owned terms available after reload; automatic restoration of those host inputs is not guaranteed. If fundingQuoteReview is present, show both destination minima and obtain explicit review before calling its accept() action. That review cannot widen the source cap or authorize another transfer.

Updating the SDK from now on

  1. Start from fresh SDK main and change the shared owner. Audit affected stock and controlled consumers instead of applying equivalent patches in each widget or app.
  2. Run focused regressions and the SDK’s required bun test, bun run typecheck, and bun run build. Validate affected app builds and rendered refresh/error behavior. Exercise authorized financial cases across relevant wallet modes, routes and recovery states.
  3. Record the exact tested source, package artifact, app/configuration and coverage. A prior candidate’s financial results do not automatically apply to a later candidate or merged main.
  4. If core changes are required, release core before updating the UI dependency. An inventory or controller-only fix does not inherently require a core release. Coordinate API changes before their consumers rely on them.
  5. Publish an approved prerelease with an explicit non-latest tag, verify its registry artifact, and pin that exact version in the test app. Verify the deployed bundle as well as the lockfile.
  6. Treat stable promotion, individual client adoption and docs targeting as separate release steps. Choose stable versioning based on behavioral breaks as well as type changes; the candidate’s patch-like numbering is not evidence of a drop-in patch upgrade. Match the docs’ SDK drift check to the documented package when promoting a version.
The refactor itself adds no automatic npm publishing. It also does not authorize publishing, deployment or wallet transactions on behalf of an integrator.

Evidence and limits

The .16 candidate passed 978 SDK tests, typecheck/build, rendered filtering tests and authenticated Account refresh checks. The preceding .15 candidate completed 28 financial cases and an additional outage/reload withdrawal test in a test integration. That full financial matrix was not rerun on .16. Frozen builds of five consumers passed on .15, while existing full-typecheck debt remained in two. These results support the tested cases; they do not establish settlement or caller compatibility for every client. See the SDK repository’s CONSISTENCY-MIGRATION.md for the release evidence and open limits.