# Stable Build #2: Build a Neobank

By [Stable](https://blog.stable.xyz) · 2026-07-09

---

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](https://stable.xyz?utm_campaign=stable-build-02-neobank), 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](https://docs.stable.xyz/en/reference/sdk?utm_campaign=stable-build-02-neobank) call that implements it.

So what would the tiniest neobank look like?
--------------------------------------------

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](https://docs.stable.xyz/en/reference/sdk?utm_campaign=stable-build-02-neobank) 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.

How do you handle user accounts?
--------------------------------

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.

How do users deposit money?
---------------------------

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, and`bridge` executes against that exact quote, moving the funds through [LI.FI](http://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.

Where does yield come from?
---------------------------

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.

How do withdrawals and payouts work?
------------------------------------

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.

Architecture at a glance
------------------------

**Component**

**SDK surface**

**What the SDK abstracts**

Accounts

`createStable, createStableReader`

Signing plumbing; reads without a key

Deposits

`stable.bridge, stable.quoteBridge`

Routing, approvals, chain switching

Conversion

`stable.swap, stable.quoteSwap`

Approvals, output quoting

Moving money

`stable.transfer`

Gas in USDT0, on-chain decimals

Yield

`stable.earn.deposit, reader.earn.getYield / .preview`

Approvals, permits, share math

Balance sweeps

`stable.earn.bulkDeposit / .bulkWithdraw`

Per-item results, batch dedup

Rewards

`stable.earn.incentiveRewards.fetch / .claim`

Merkl proofs, multi-token claims

Withdrawals

`reader.earn.withdrawability, stable.earn.withdraw / .redeem`

Liquidity checks, share accounting

Get started
-----------

`npm install @stablechain/sdk viem` and you have the client. The [SDK quickstart](https://docs.stable.xyz/en/tutorial/sdk-quickstart?utm_campaign=stable-build-02-neobank) walks through your first transfer and deposit on testnet.

Start building
--------------

The [SDK reference](https://docs.stable.xyz/en/reference/sdk?utm_campaign=stable-build-02-neobank) is the canonical surface for every method and parameter. If you're building on Stable, find us in [Discord](https://discord.gg/stablexyz) and tell us what you're shipping.

---

*Originally published on [Stable](https://blog.stable.xyz/stable-build-2-build-a-neobank)*
