A neobank is a bank delivered as an app. No branches, just a balance and the software that moves it.
A stablecoin neobank keeps that balance in dollars on a chain. Transfers settle in seconds instead of days, a payout to another country is the same call as a payout across town, and yield comes from onchain lending markets rather than a treasury desk.
Building one usually means stitching together a bridge provider, a payments rail, a yield protocol, and gas management, each with its own SDK, decimals, and failure modes. On Stable, those pieces sit behind one client.
If you're shipping a global spending app, a payroll product, a savings app for users whose local currency is the problem, or a card program, you're building the same five components. This post maps each one to the @stablechain/sdk call that implements it.
The tiniest neobank on Stable is a single call: an amount moving between two addresses, with the fee settled in the same dollars it moves.
That works because the balance asset here is USDT0, the multichain variant of USDT, and gas is denominated in it too. A user with fifty dollars can spend fifty dollars. There is no native token to acquire first, and no gas balance to explain in your onboarding flow.
The SDK is moving quickly. The snippets below are accurate at the time of writing, but the SDK reference is always the source of truth.
const { txHash } = await stable.transfer({
from: "0xSender",
to: "0xRecipient",
amount: 10,
});{ txHash: "0x8f3a...2d41" }The SDK fetches token decimals on-chain and settles gas in USDT0 automatically. amount: 10 means ten dollars.
An account on Stable is an address plus a signer, and who holds the key is a product decision the chain doesn't make for you.
An embedded wallet gives each user their own signer; a custodial backend keeps one signer and an internal ledger. The SDK doesn't care which you pick, because createStable takes any viem-compatible account. What it does separate for you is the write path from the read path: dashboards and balance screens shouldn't need a key, so createStableReader builds a read-only client from nothing but an address.
import { createStable, createStableReader, Network, STABLE_VAULT_ADDRESS } from "@stablechain/sdk";
// Write path: any viem-compatible signer
const stable = createStable({ network: Network.Mainnet, account });
// Read path: an address is enough
const reader = createStableReader({
network: Network.Mainnet,
earn: { vault: STABLE_VAULT_ADDRESS },
address: user.address,
});
const { assets } = await reader.earn.position();{ assets: 1250.42, sharesFormatted: 1248.9, assetAddress: "0x779D...3736", ... }The reader needs no key and no connected wallet, which is exactly what a backend rendering ten thousand balance screens wants.
Users deposit by bridging stablecoins they already hold, whether sitting in a wallet on another chain or withdrawn from an exchange, into USDT0 on Stable; a fiat on-ramp lands the same way, one hop earlier.
Most of your early users already have USDT somewhere, and exchanges are the most common somewhere. A withdrawal to Ethereum mainnet puts that USDT exactly where the snippet below picks it up. From there it's a two-call pattern: quoteBridge returns the expected output for the user to confirm, andbridge executes against that exact quote, moving the funds through LI.FI.
import { CHAIN_CONFIGS, Chain, STABLE_USDT_ADDRESS } from "@stablechain/sdk";
const params = {
fromChain: Chain.Ethereum,
toChain: Chain.Stable,
fromToken: CHAIN_CONFIGS[Chain.Ethereum]!.usdt, // USDT on Ethereum
toToken: STABLE_USDT_ADDRESS, // USDT0 on Stable
amount: 100,
};
const quote = await stable.quoteBridge(params);
// show quote.toAmount, then execute against the quote the user confirmed
const { txHash } = await stable.bridge({ ...params, quote });{ toAmount: 99.94 }
{ txHash: "0xabcd...7890" }Pass recipient to land the funds in the user's address rather than the signer's, which is how a treasury-funded onboarding flow works. If a user arrives holding something that isn't USDT, swap routes it into USDT0 inside the same client. Routing, approvals, and chain switching are handled internally.
Yield comes from lending: idle USDT0 deposits into a Morpho-backed vault (StableEarn) that supplies onchain lending markets, and the vault's rate is what your users see as their savings APY.
This is the savings-account leg of the product. deposit moves a balance into the vault, the reader hands the live position and APY back for the dashboard, and preview projects yield over a horizon, which is the "deposit $1,000, earn about $58 this year" line every savings app puts on its deposit screen. How you surface those numbers to end users is a compliance decision in several jurisdictions; the SDK gives you the figures, the display is your call. Merkl incentive rewards stack on top: incentiveRewards.fetch and .claim collect every reward token in one transaction, proofs included, and the reader exposes the same fetch without a signer.
const { txHash } = await stable.earn.deposit({ amount: 100 });
const { apy } = await reader.earn.getYield();
const { projectedYield } = await reader.earn.preview({ amount: 1000, horizonDays: 365 });{ txHash: "0x41c7...9be2" }
{ apy: 0.058, native: 0.041, rewards: [...], ... }
{ projectedYield: 58.31, apy: 0.058, horizonDays: 365 }At scale you won't deposit one balance at a time. A nightly sweep is bulkDeposit: it takes the whole batch, attempts every item, and returns per-item results, so three failed accounts don't sink the other ten thousand.
const { succeeded, failed, results } = await stable.earn.bulkDeposit(
idleBalances.map((amount) => ({ amount })),
{ idempotencyKey: "sweep-2026-07-07" },
);{ succeeded: 9997, failed: 3, results: [...] }Approvals, permit signatures, and share math happen behind the scenes. Pass idempotencyKey so a retried call, or a re-run sweep, joins the in-flight operation instead of sending a second one.
Withdrawals pull USDT0 out of the vault by asset amount (withdraw) or by share count (redeem), and a payout from there is a transfer on Stable or a bridge back out to another chain. A payroll run is the same shape at batch size: bulkWithdraw frees the balances, then the transfers go out.
Vault liquidity is the one place a savings product can surprise its users, so check it first. withdrawability tells you whether an amount exits instantly and how much idle liquidity the vault holds; when idle liquidity is short, forceWithdraw and forceRedeem deallocate from the underlying markets in the same call.
const { isInstant, availableLiquidity } = await reader.earn.withdrawability({ amount: 500 });
if (isInstant) {
await stable.earn.withdraw({ amount: 500 });
}{ isInstant: true, availableLiquidity: 182304.11, tokenDecimals: 6 }Share-to-asset conversion and vault accounting are handled for you. The payout itself is the same transfer call from the first section.
Component | SDK surface | What the SDK abstracts |
|---|---|---|
Accounts |
| Signing plumbing; reads without a key |
Deposits |
| Routing, approvals, chain switching |
Conversion |
| Approvals, output quoting |
Moving money |
| Gas in USDT0, on-chain decimals |
Yield |
| Approvals, permits, share math |
Balance sweeps |
| Per-item results, batch dedup |
Rewards |
| Merkl proofs, multi-token claims |
Withdrawals |
| Liquidity checks, share accounting |
npm install @stablechain/sdk viem and you have the client. The SDK quickstart walks through your first transfer and deposit on testnet.
The SDK reference is the canonical surface for every method and parameter. If you're building on Stable, find us in Discord and tell us what you're shipping.

