Skip to content

Repository files navigation

SmoothYield — Add BTC Staking to Any TradFi App with Starkzap

SmoothYield is an open-source reference app that shows web2 developers how to add passive BTC yield to a traditional finance (TradFi) mobile app — no seed phrases, no gas fees, no blockchain UX. The entire crypto layer is powered by Starkzap, the official Starknet developer toolkit.

TL;DR — Your users sign in with Google. Behind the scenes, Starkzap creates an invisible Starknet wallet and stakes LBTC for ~3.3% APY. All transactions are gasless (sponsored by the AVNU paymaster). Your users never see a wallet address, sign a transaction, or pay gas. They just see "Earning 3.33% APY on BTC".


Table of Contents

  1. Why Add Crypto Rails to a TradFi App?
  2. Architecture Overview
  3. Starkzap Modules Used
  4. Step-by-Step: How Starkzap Is Integrated
  5. The Paymaster: Gasless Transactions
  6. Project Structure
  7. Quick Start
  8. Environment Variables
  9. API Reference
  10. End-to-End Yield Flow
  11. Troubleshooting
  12. Resources

Why Add Crypto Rails to a TradFi App?

Traditional finance apps (stock brokers, neobanks, portfolio trackers) can unlock new revenue streams and user retention by offering passive crypto yield — without rebuilding the app as a "crypto app". Here is what Starkzap enables:

Benefit How It Works
No wallet UX Privy creates invisible embedded wallets. Users sign in with Google/email — no MetaMask, no seed phrases.
No gas fees The AVNU paymaster sponsors every transaction. Your users never need to hold ETH or STRK for gas.
Real BTC yield LBTC staking on Starknet earns ~3.3% APY through native delegation to Starknet validators.
4 npm packages starkzap + @privy-io/expo + @privy-io/node + starknet. That's the full crypto stack.
Backend-only signing All transaction signing happens on your server. The mobile app never touches private keys.

Your users see a TradFi app with stocks, cash, and a "BTC Yield" tab. Behind the scenes, Starkzap handles wallets, staking, and gas — all in a few function calls.


Architecture Overview

┌──────────────────────────────────────────────────────────────────┐
│                         Mobile App (Expo)                        │
│                                                                  │
│   PrivyProvider → Google OAuth → AuthContext (sessionId)         │
│   Screens: Stocks | Crypto | Portfolio | Profile                 │
│   YieldScreen: "Stake BTC now" button                           │
│                                                                  │
│   No wallet addresses shown.  No gas prompts.  No crypto UX.    │
└─────────────────────────────┬────────────────────────────────────┘
                              │  REST API (x-session-id)
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│                       Backend (Express)                          │
│                                                                  │
│   Auth:  Privy token verification → session                      │
│   Wallet: Privy server SDK → create Starknet wallet              │
│   Stake: Starkzap Staking → delegate LBTC to validator pool      │
│   Gas:   AVNU Paymaster → all transactions sponsored             │
│   Sign:  Privy rawSign → server-side, no user interaction        │
│                                                                  │
│   Key service: backend/src/services/starkzap.ts                  │
└─────────────────────────────┬────────────────────────────────────┘
                              │  Starkzap SDK + Paymaster
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│                         Starknet                                 │
│                                                                  │
│   Account deployment (sponsored)                                 │
│   ERC-20 token transfers (LBTC)                                  │
│   Native staking (LBTC → validator pool → yield)                │
└──────────────────────────────────────────────────────────────────┘

Starkzap Modules Used

SmoothYield imports these modules from the starkzap npm package:

Module Purpose Where Used
StarkZap / StarkSDK Main SDK class — initializes RPC, paymaster, network config backend/src/services/starkzap.ts
OnboardStrategy Enum for wallet onboarding strategies (Privy, Argent, etc.) mobile/src/starknet/useStarknetWallet.ts
accountPresets Preconfigured account types (e.g. argentXV050) mobile/src/starknet/useStarknetWallet.ts
WalletInterface TypeScript type for the onboarded wallet object mobile/src/starknet/useStarknetWallet.ts
mainnetTokens / sepoliaTokens Token presets (USDC, LBTC addresses and decimals) backend/src/services/starkzap.ts
Staking Staking class — fromPool, populateEnter, populateAdd, populateExitIntent, populateExit, getStakerPools, isMember backend/src/adapters/staking.ts
Amount Token amount helper — fromRaw, toBase, toUnit backend/src/adapters/staking.ts
ChainId Chain identifier enum (MAINNET, SEPOLIA) backend/src/adapters/staking.ts, starkzap.ts
getStakingPreset Returns staking configuration for a given chain backend/src/adapters/staking.ts, starkzap.ts
mainnetValidators / sepoliaValidators Validator registry for LBTC pool discovery backend/src/services/starkzap.ts

The Starkzap SDK also handles:

  • Paymaster integration — pass your AVNU API key and all wallet.execute() calls become gasless.
  • Privy onboardingsdk.onboard({ strategy: "privy", ... }) connects a Privy-managed wallet to Starkzap in one call.
  • Account deploymentwallet.deploy({ feeMode: "sponsored" }) deploys the account contract on-chain, gas-free.

Step-by-Step: How Starkzap Is Integrated

This section walks through every integration point so you can replicate it in your own app. Each step includes the actual code from this repository.

Step 1: Install Starkzap

Add the starkzap package to both your backend and frontend:

# Backend
cd backend
npm install starkzap starknet @privy-io/node

# Mobile / Frontend
cd mobile
npm install starkzap @privy-io/expo

That's it. No complex blockchain toolchains, no Solidity compilers, no ABIs. Starkzap is a pure JavaScript/TypeScript SDK.

Step 2: Get Your API Keys

You need three services — all have free tiers:

Service What It Does Where to Get It
Privy User auth + embedded wallets dashboard.privy.io
AVNU Paymaster (gas sponsorship) portal.avnu.fi
Alchemy (optional) Starknet RPC node dashboard.alchemy.com

Set them in your backend .env:

PRIVY_APP_ID=your-privy-app-id
PRIVY_APP_SECRET=your-privy-app-secret
AVNU_PAYMASTER_API_KEY=your-avnu-key
ALCHEMY_STARKNET_API_KEY=your-alchemy-key   # optional
STARKNET_NETWORK=sepolia                     # or mainnet

Step 3: Initialize the Starkzap SDK

The SDK is initialized once and reused. The key configuration is the paymaster — this is what makes every transaction gasless.

import("starkzap").then((starkzap) => {
  const sdk = new starkzap.StarkZap({
    network: "sepolia",                    // or "mainnet"
    rpcUrl: "https://starknet-sepolia.g.alchemy.com/v2/YOUR_KEY",

    // This is the magic: AVNU paymaster makes all txs gasless
    paymaster: {
      nodeUrl: "https://sepolia.paymaster.avnu.fi",
      headers: { "x-paymaster-api-key": "YOUR_AVNU_KEY" },
    },
  });
});

Docs: Starkzap Paymasters and AVNU Paymaster Integration

In SmoothYield, this lives in backend/src/services/starkzap.ts — the getSdk() function lazily initializes it:

async function getSdk() {
  if (_sdk) return _sdk;
  const starkzap = await import("starkzap");
  const Cls = starkzap.StarkSDK ?? starkzap.StarkZap;
  // ... RPC URL resolution ...

  const paymaster =
    config.avnuPaymasterApiKey
      ? {
          nodeUrl:
            config.network === "sepolia"
              ? "https://sepolia.paymaster.avnu.fi"
              : "https://starknet.paymaster.avnu.fi",
          headers: { "x-paymaster-api-key": config.avnuPaymasterApiKey },
        }
      : undefined;

  _sdk = new Cls({
    ...sdkConfig,
    ...(paymaster && { paymaster }),
  });
  return _sdk;
}

Step 4: Create Invisible Wallets with Privy

When a user signs in with Google, your backend creates a Starknet wallet for them using Privy's server SDK. The user never sees this wallet.

import { PrivyClient } from "@privy-io/node";

const privy = new PrivyClient({
  appId: "your-privy-app-id",
  appSecret: "your-privy-app-secret",
});

// One line to create a Starknet wallet
const wallet = await privy.wallets().create({
  chain_type: "starknet",
});

// wallet.id       → "wallet_abc123"  (used for signing)
// wallet.address  → "0x04a3..."      (Starknet address)
// wallet.public_key → "0x02b1..."    (needed for Starkzap onboard)

Docs: Starkzap Privy Integration

In SmoothYield, this lives in backend/src/services/privyWallet.ts:

export async function createStarknetWallet(_privyUserId: string): Promise<PrivyWallet> {
  const privy = getPrivyClient();
  const wallet = await privy.wallets().create({
    chain_type: "starknet",
  });
  const address = normalizeStarknetAddress(wallet.address);
  return {
    id: wallet.id,
    address,
    public_key: wallet.public_key ?? "",
    chain_type: wallet.chain_type,
  };
}

Step 5: Onboard a Wallet into Starkzap

This is the central integration point. sdk.onboard() connects the Privy wallet to Starkzap so it can execute transactions (staking, transfers) — all server-side.

const wallet = await sdk.onboard({
  strategy: "privy",
  privy: {
    resolve: async () => ({
      walletId: "wallet_abc123",              // from Privy
      publicKey: "0x02b1...",                  // from Privy
      serverUrl: "https://your-api.com/api/wallet/sign",  // your sign endpoint
    }),
  },
  deploy: "never",          // deploy on first real tx, not eagerly
  feeMode: "sponsored",     // gasless via paymaster
});

// wallet can now execute(), balanceOf(), deploy(), etc.

The serverUrl points to your backend endpoint that signs transaction hashes using Privy's rawSign:

// POST /api/wallet/sign — Starkzap calls this automatically
const result = await privy.wallets().rawSign(walletId, {
  params: { hash: "0x..." },
});
return { signature: result.signature };

Docs: Starkzap Connecting Wallets and Privy Integration

In SmoothYield, the onboard function is in backend/src/services/starkzap.ts:

export const onboardPrivyWallet = async (
  _privyUserId: string,
  privyWalletId: string,
  publicKey: string,
  signEndpointUrl: string
) => {
  if (!config.avnuPaymasterApiKey) {
    throw new Error(
      "AVNU_PAYMASTER_API_KEY is required for gasfree (sponsored) deploy and execute. Set it in backend .env. See https://docs.starknet.io/build/starkzap/paymasters"
    );
  }
  const sdk = await getSdk();
  if (!sdk?.onboard) throw new Error("Starkzap SDK onboard unavailable");

  const result = await sdk.onboard({
    strategy: "privy",
    privy: {
      resolve: async () => ({
        walletId: privyWalletId,
        publicKey,
        serverUrl: signEndpointUrl,
      }),
    },
    deploy: "never",
    feeMode: "sponsored",
  });
  return result?.wallet ?? result;
};

Step 6: Read Token Balances

Starkzap provides token presets with correct addresses and decimals for each network. Use them to read balances:

const starkzap = await import("starkzap");

// Get token presets for the current network
const tokens = starkzap.sepoliaTokens; // or mainnetTokens

// tokens.USDC  → { address: "0x...", decimals: 6, symbol: "USDC" }
// tokens.LBTC  → { address: "0x...", decimals: 8, symbol: "LBTC" }

// Read balance using the onboarded wallet
const usdcBalance = await wallet.balanceOf(tokens.USDC);
console.log(usdcBalance.toBase());  // raw amount (e.g. "1000000" = 1 USDC)

In SmoothYield, getTokenAddresses() resolves tokens from Starkzap presets and supports optional address overrides via environment variables:

export const getTokenAddresses = async (): Promise<{
  usdc: Token;
  usdcBridged: Token | null;
  lbtc: Token;
}> => {
  const preset = await getPresetTokens();
  const usdc = config.usdcTokenAddress
    ? tokenOverride(config.usdcTokenAddress, preset.USDC)
    : preset.USDC;
  const usdcBridged =
    "USDC_E" in preset && preset.USDC_E && !config.usdcTokenAddress
      ? preset.USDC_E
      : null;
  const lbtc = config.lbtcTokenAddress
    ? tokenOverride(config.lbtcTokenAddress, preset.LBTC)
    : preset.LBTC;
  return { usdc, usdcBridged, lbtc };
};

Step 7: Stake LBTC for Yield

This is where the real magic happens. Starkzap's Staking module lets you stake LBTC in a validator's delegation pool to earn yield — all in a few lines.

const starkzap = await import("starkzap");
const { Staking, Amount, ChainId, getStakingPreset } = starkzap;

// 1. Get the staking configuration for this network
const chainId = ChainId.SEPOLIA;   // or ChainId.MAINNET
const stakingConfig = getStakingPreset(chainId);

// 2. Connect to a validator's LBTC staking pool
const staking = await Staking.fromPool(poolAddress, provider, stakingConfig);

// 3. Build the staking calls
const amount = Amount.fromRaw("100000000", 8, "LBTC");  // 1 LBTC
const isMember = await staking.isMember(wallet);

const calls = isMember
  ? staking.populateAdd(walletAddress, amount)      // already staking → add more
  : staking.populateEnter(walletAddress, amount);   // first time → enter pool

// 4. Execute gaslessly
const tx = await wallet.execute(calls, { feeMode: "sponsored" });

Pool discovery is automatic — Starkzap provides a validator registry:

const validators = starkzap.sepoliaValidators;   // or mainnetValidators
for (const validator of Object.values(validators)) {
  const pools = await Staking.getStakerPools(provider, validator.stakerAddress, stakingConfig);
  const lbtcPool = pools.find(p => p.token.symbol === "LBTC");
  if (lbtcPool) {
    // lbtcPool.poolContract is your pool address
  }
}

In SmoothYield, the full staking flow is in backend/src/adapters/staking.ts:

  // Native staking via Starkzap: build approve + enter/add calls, normalize for paymaster, then execute
  const starkzap = await import("starkzap");
  const { Staking, Amount, ChainId, getStakingPreset } = starkzap;
  const provider = getProvider();
  const chainId = config.network === "sepolia" ? ChainId.SEPOLIA : ChainId.MAINNET;
  const stakingConfig = getStakingPreset(chainId);
  // ...
  const staking = await Staking.fromPool(poolAddress as any, provider as any, stakingConfig);
  // ...
  const rawCalls = isMember
    ? staking.populateAdd(walletAddress, amountObj)
    : staking.populateEnter(walletAddress, amountObj);

  const tx = await sendSponsoredTx(wallet, rawCalls);
  // ...

Step 8: Unstake and Withdraw

Starknet staking has a two-phase exit: first declare intent, then withdraw after the cooldown.

// Phase 1: Exit intent — "I want to unstake X LBTC"
const amount = Amount.fromRaw("100000000", 8, "LBTC");
const exitIntentCall = staking.populateExitIntent(amount);
await wallet.execute([exitIntentCall], { feeMode: "sponsored" });

// Phase 2: Exit — after cooldown period passes
const exitCall = staking.populateExit(walletAddress);
await wallet.execute([exitCall], { feeMode: "sponsored" });

In SmoothYield, these are the POST /api/yield/unstake and POST /api/yield/exit endpoints in backend/src/adapters/staking.ts.


The Paymaster: Gasless Transactions

The AVNU paymaster is what makes this app feel like a traditional fintech app. Without it, every transaction would require the user to hold STRK or ETH for gas fees — a dealbreaker for TradFi users.

How it works:

  1. You get an API key from portal.avnu.fi
  2. You pass it to the Starkzap SDK at initialization
  3. Every wallet.execute(calls, { feeMode: "sponsored" }) call is gasless
  4. AVNU pays the gas. You can set up billing with them for production.

What's sponsored:

  • Account deployment (wallet.deploy({ feeMode: "sponsored" }))
  • Staking deposits and withdrawals
  • Any multicall transaction

Configuration in SmoothYield:

Network Paymaster URL
Sepolia (testnet) https://sepolia.paymaster.avnu.fi
Mainnet https://starknet.paymaster.avnu.fi

Docs: Starkzap Paymasters · AVNU Paymaster Integration


Project Structure

smoothyield/
├── backend/                          # Express API server
│   ├── src/
│   │   ├── services/
│   │   │   ├── starkzap.ts          # Starkzap SDK init, onboard, balances, sponsored tx
│   │   │   ├── privyWallet.ts       # Privy wallet creation + rawSign
│   │   │   └── privy.ts             # Privy token verification
│   │   ├── adapters/
│   │   │   └── staking.ts           # Starkzap Staking: stake, unstake, exit
│   │   ├── routes/index.ts          # All API endpoints
│   │   ├── config.ts                # Environment configuration
│   │   ├── db.ts                    # SQLite (sessions, wallets, yield)
│   │   └── middleware.ts            # Session auth middleware
│   ├── .env.example
│   └── package.json
│
├── mobile/                           # Expo React Native app
│   ├── app/
│   │   ├── _layout.tsx              # PrivyProvider + AuthProvider
│   │   └── (tabs)/                  # Tab navigation (stocks, crypto, portfolio, profile)
│   ├── src/
│   │   ├── context/AuthContext.tsx   # Privy auth → backend session
│   │   ├── screens/
│   │   │   ├── YieldScreen.tsx      # BTC staking UI
│   │   │   ├── CryptoScreen.tsx     # Crypto balances + markets
│   │   │   ├── PortfolioScreen.tsx  # TradFi + crypto holdings
│   │   │   └── LoginScreen.tsx      # Google OAuth login
│   │   ├── starknet/
│   │   │   ├── useStarknetWallet.ts # Client-side Starkzap onboard hook
│   │   │   └── walletClient.ts      # Wallet fetch + Privy resolve builder
│   │   └── components/              # UI components (StakingCard, BalanceCard, etc.)
│   ├── .env.example
│   └── package.json
│
├── types/                            # Shared TypeScript types
└── package.json                      # Workspace root

Quick Start

1. Backend

cd backend
cp .env.example .env
# Edit .env — set PRIVY_APP_ID, PRIVY_APP_SECRET, AVNU_PAYMASTER_API_KEY
npm install
npm run dev

2. Mobile

cd mobile
cp .env.example .env
# Edit .env — set EXPO_PUBLIC_PRIVY_APP_ID and EXPO_PUBLIC_API_URL
npm install
npm run start

On a physical device: use your machine's LAN IP for EXPO_PUBLIC_API_URL (e.g. http://192.168.1.5:3000) since localhost on the device refers to the device itself.

3. Dev Mode

Set DEV_MODE=true in backend/.env to mock auth/staking for local testing without real credentials. The full yield flow can be tested end-to-end with mock transactions.


Environment Variables

Backend (backend/.env)

Variable Required Description
PRIVY_APP_ID Yes From dashboard.privy.io
PRIVY_APP_SECRET Yes From Privy dashboard
AVNU_PAYMASTER_API_KEY Yes From portal.avnu.fi — enables gasless transactions
ALCHEMY_STARKNET_API_KEY No Starknet RPC via Alchemy (recommended for production)
STARKNET_RPC_URL No Override RPC URL entirely
STARKNET_NETWORK No sepolia (default) or mainnet
DEV_MODE No true for mock mode
BTC_STAKING_APY No Override displayed APY (default: 3.33)
PORT No Server port (default: 3000)

Mobile (mobile/.env)

Variable Required Description
EXPO_PUBLIC_PRIVY_APP_ID Yes Same Privy App ID
EXPO_PUBLIC_API_URL Yes Backend URL (e.g. http://localhost:3000)

API Reference

Method Endpoint Auth Description
GET /health Public Health check
GET /api/market-data Public Stock + crypto market prices
POST /api/auth/session Public Exchange Privy token for session
POST /api/wallet/starknet Bearer Get/create Starkzap-compatible wallet
POST /api/wallet/init Session Initialize wallet + optional deploy
POST /api/wallet/sign Key/Bearer Sign transaction hash (Starkzap callback)
GET /api/wallet/address Session Get wallet address
POST /api/wallet/deploy Session Deploy account on-chain (sponsored)
GET /api/portfolio Session TradFi + crypto balances + yield state
POST /api/yield/stake Session Stake LBTC for yield
POST /api/yield/unstake Session Exit intent (begin unstaking)
POST /api/yield/exit Session Finalize withdrawal after cooldown
GET /api/tx/:hash Session Check transaction status

End-to-End Yield Flow

Here is what happens when a user taps "Stake BTC now" in the app:

User taps "Stake BTC now"
        │
        ▼
1. POST /api/wallet/init
   └─ Backend creates Privy wallet (if needed)
   └─ Calls sdk.onboard({ strategy: "privy" })
   └─ Optionally deploys account (sponsored)
        │
        ▼
2. POST /api/yield/stake { amountLbtc: "100000000" }
   └─ Backend onboards wallet via Starkzap
   └─ Resolves LBTC pool via Starkzap validators
   └─ Calls Staking.fromPool() → staking.populateEnter()
   └─ Executes via wallet.execute(calls, { feeMode: "sponsored" })
   └─ Starkzap calls POST /api/wallet/sign to get signature
   └─ AVNU paymaster pays gas
   └─ Transaction lands on Starknet
        │
        ▼
3. App shows "Earning 3.33% APY on BTC"
   └─ Links to Voyager explorer for the tx

The user saw: one button tap. Behind the scenes: wallet creation, account deployment, pool discovery, staking transaction construction, server-side signing, gas sponsorship, and on-chain execution — all handled by Starkzap + Privy + AVNU.


Troubleshooting

"Network request failed" on mobile

The app uses EXPO_PUBLIC_API_URL from your .env. Common issues:

  1. localhost on a real device — use your machine's LAN IP instead (e.g. http://192.168.1.5:3000)
  2. Backend not running — run cd backend && npm run dev
  3. Wrong port — check PORT in backend/.env, default is 3000

Quick check: open EXPO_PUBLIC_API_URL/health in a browser — you should see {"ok":true}.

"AVNU_PAYMASTER_API_KEY is required"

All DeFi operations (stake, deploy) require the paymaster. Get a free key from portal.avnu.fi.

"No LBTC staking pool found"

Native LBTC staking may not be available on Sepolia. Try mainnet (STARKNET_NETWORK=mainnet) or set BTC_STAKING_CONTRACT_ADDRESS for a custom contract.


Resources


Built with Starkzap — the fastest way to add Starknet to any app.

About

A TradFI mockup app with real BTC staking integrating, shopwing how Web2 TradFi apps can easily get on crypto reails with Starkzap, and earn additional revenue

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages