Yield

Read APR and pending yield, switch the active yield asset, and claim accrued yield on AA wallets.

Before You Start

Read the following guides before proceeding:

GuideWhy
Getting StartedPlatform overview and setup
Api BasicsRequired headers and request configuration
OnboardingUser and wallet registration
Unified BalanceWUSD/WEUR token mechanics
ABI ReferenceSmart contract ABIs

Overview

Yield accrues directly on the balance the user already holds — WUSD or WEUR. Users do not move funds into a separate vault, and the balance stays fully card-usable the whole time. There is no lock-up and no "earn vs. spendable" split: the same balance earns yield and can be spent on the card.

  • Every wallet accrues yield on WUSD by default.
  • The active asset can be switched to WEUR (and back to WUSD) at any time via the YieldCalculator contract.
  • Only one asset is the active yield asset per wallet at a time.
  • Accrual begins the moment a wallet is created and funded.
  • The yield rate is flexible. Read the live rate on-chain or via the SDK rather than assuming a fixed value.

Yield is a Base on-chain feature on the unified tokens. WUSD/WEUR do not apply on Stellar.


How It Works

Yield is managed by two contracts:

ContractRole
YieldCalculatorTracks the active yield asset per wallet and computes APR
WUSD / WEUR (unified token)Holds the balance, accrues yield, and processes claims

The flow:

  1. Wallet is created and funded with WUSD/WEUR.
  2. Yield accrues on the active asset from that moment (WUSD by default).
  3. Optionally, switch the active asset to WEUR via YieldCalculator.setActiveYieldToken().
  4. Read pending yield at any time; claim it when it reaches the minimum.
  5. Claimed yield is credited to the wallet's balance and remains card-usable.

State-changing actions (setActiveYieldToken, claimYield) run as transactions from the user's AA (account abstraction) wallet. Read functions are plain view calls.

You can integrate either through the Wirex SDK (recommended — it wraps the reads and AA transactions) or by calling the contracts directly.


Using the Wirex SDK (Recommended)

The @wirexapp/wpay-baas-sdk exposes yield through sdk.crypto.yield. The script below is complete and runnable — it initializes the SDK, reads yield data, switches the active asset, and claims. All methods operate on the wallet's active yield asset.

For full SDK setup and the MainWalletClient adapter — including embedded wallets such as Privy — see the SDK guide. The script below uses a raw EOA key for brevity.

import { createWalletClient, http, type Hex } from 'viem';
import { base, baseSepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import { createSDK, type MainWalletClient, type WirexPaySDK } from '@wirexapp/wpay-baas-sdk';

const ENV = 'dev';                                  // 'dev' → Base Sepolia | 'prod' → Base
const COMPANY_ID = process.env.COMPANY_ID as string; // hex company ID from onboarding
const PRIVATE_KEY = process.env.PRIVATE_KEY as Hex;  // user's EOA private key
const RPC_URL = ENV === 'prod' ? 'https://mainnet.base.org' : 'https://sepolia.base.org';

// Wrap a raw EOA key as a MainWalletClient. In production you typically pass an
// embedded-wallet client (e.g. Privy) instead — see the SDK guide linked above.
function toMainWalletClient(privateKey: Hex): MainWalletClient {
  const account = privateKeyToAccount(privateKey);
  const chain = ENV === 'prod' ? base : baseSepolia;
  const client = createWalletClient({ account, chain, transport: http(RPC_URL) });
  return {
    address: account.address,
    getEthereumProvider: async () => ({
      request: async ({ method, params = [] }: any) => {
        if (method === 'eth_accounts') return [account.address];
        if (method === 'personal_sign') {
          const [data] = params;
          return client.signMessage({ account, message: { raw: data } });
        }
        if (method === 'eth_signTypedData_v4') {
          const [, json] = params;
          const { domain, types, primaryType, message } = JSON.parse(json);
          const { EIP712Domain, ...rest } = types;
          return client.signTypedData({ account, domain, types: rest, primaryType, message });
        }
        return (client.transport as any).request({ method, params });
      },
    }),
  };
}

async function main() {
  const sdk: WirexPaySDK = await createSDK({
    env: ENV,
    companyId: COMPANY_ID,
    getMainWalletClient: () => toMainWalletClient(PRIVATE_KEY),
  });

  // 1. Read yield data for the active asset
  const data = await sdk.crypto.yield.fetchYieldData();
  console.log('APY (%):      ', data.apy);                 // flexible
  console.log('Pending:      ', data.currentPendingYield); // available to claim, active token
  console.log('Weekly (proj):', data.weeklyYield);         // projected next 7 days
  console.log('Lifetime:     ', data.lifeTimeYield);       // claimed + pending, all time

  // 2. Switch the active yield asset (WUSD ⇄ WEUR)
  const weur = sdk.crypto.getToken('WEUR'); // or 'WUSD'
  await sdk.crypto.yield.switchYieldToken(weur);

  // 3. Claim accrued yield to the wallet balance (minimum 1 USD)
  await sdk.crypto.yield.withdraw();
}

main().catch(console.error);

Yield Service Methods

MethodReturnsDescription
fetchYieldData(){ apy, currentPendingYield, weeklyYield, lifeTimeYield }APY (%) plus pending, weekly, and lifetime yield
getWeeklyYield()stringProjected yield over the next 7 days
getYieldDataForGraphic(){ timestamp, yield }[]Time series for charting (see note below)
switchYieldToken(token)voidSwitch the active asset; pass a TokenInfo from sdk.crypto.getToken()
withdraw()voidClaim accrued yield (minimum 1 USD)

fetchYieldData().apy is a number in percent (the rate is flexible). getYieldDataForGraphic() reads historical logs, so point the SDK at an RPC that serves full history — the default RPC may prune older logs.


Direct Contract Interaction

The sections below cover calling the yield contracts directly, for partners who do not use the Wirex SDK. The SDK methods above wrap exactly these calls.


Contract Addresses

Production (Base Mainnet — Chain ID 8453)

ContractAddress
YieldCalculator0xEd8424728A90A11b7531422AbD9FF7bbB9bEB562
WUSD0xb4bB2032A73A53C0Aa7Dc9ee2d9658a978fA7bC2
WEUR0x379e120C1921bFD8f5E0A1a3C699e7e800b66606

Sandbox (Base Sepolia — Chain ID 84532)

ContractAddress
YieldCalculator0x7D390c3D77792ad0EA4dC0D36B114E49f91B967a
WUSD0x0774164DC20524Bb239b39D1DC42573C3E4C6976
WEUR0x5c55F314624718019A326F16a62A05D6C6d8C8A2

ABI

YieldCalculator

[
  {
    "type": "function",
    "name": "getCurrentApr",
    "stateMutability": "view",
    "inputs": [{ "name": "synthetic", "type": "address" }],
    "outputs": [{ "name": "", "type": "uint32" }]
  },
  {
    "type": "function",
    "name": "getActiveYieldToken",
    "stateMutability": "view",
    "inputs": [{ "name": "user", "type": "address" }],
    "outputs": [{ "name": "", "type": "address" }]
  },
  {
    "type": "function",
    "name": "setActiveYieldToken",
    "stateMutability": "nonpayable",
    "inputs": [{ "name": "synthetic", "type": "address" }],
    "outputs": []
  }
]

Unified Token (WUSD / WEUR)

Both WUSD and WEUR expose the same interface. Call the read and claim functions on whichever contract is the wallet's active yield asset.

[
  {
    "type": "function",
    "name": "pendingYield",
    "stateMutability": "view",
    "inputs": [{ "name": "account", "type": "address" }],
    "outputs": [
      {
        "name": "info",
        "type": "tuple",
        "components": [
          { "name": "accumulatedYield", "type": "uint256" },
          { "name": "lastBalance", "type": "uint256" },
          { "name": "lastUpdateTime", "type": "uint64" }
        ]
      }
    ]
  },
  {
    "type": "function",
    "name": "claimedYield",
    "stateMutability": "view",
    "inputs": [{ "name": "account", "type": "address" }],
    "outputs": [{ "name": "", "type": "uint256" }]
  },
  {
    "type": "function",
    "name": "claimYield",
    "stateMutability": "nonpayable",
    "inputs": [],
    "outputs": [{ "name": "", "type": "uint256" }]
  }
]

Reading Yield On-Chain

This complete script reads the current APR, pending (claimable) yield, and total claimed yield for a wallet. Reads only need a public client.

import { createPublicClient, http } from 'viem';
import { baseSepolia } from 'viem/chains';        // production: import { base }

const YieldCalculatorAbi = [
  { type: 'function', name: 'getCurrentApr', stateMutability: 'view',
    inputs: [{ type: 'address', name: 'synthetic' }], outputs: [{ type: 'uint32' }] },
] as const;

const SyntheticTokenAbi = [
  { type: 'function', name: 'pendingYield', stateMutability: 'view',
    inputs: [{ type: 'address', name: 'account' }],
    outputs: [{ type: 'tuple', name: 'info', components: [
      { type: 'uint256', name: 'accumulatedYield' },
      { type: 'uint256', name: 'lastBalance' },
      { type: 'uint64', name: 'lastUpdateTime' },
    ] }] },
  { type: 'function', name: 'claimedYield', stateMutability: 'view',
    inputs: [{ type: 'address', name: 'account' }], outputs: [{ type: 'uint256' }] },
] as const;

// Sandbox / Base Sepolia. For production, use `base` and the Mainnet addresses
// from the Contract Addresses table above.
const YIELD_CALCULATOR = '0x7D390c3D77792ad0EA4dC0D36B114E49f91B967a' as const;
const WUSD = '0x0774164DC20524Bb239b39D1DC42573C3E4C6976' as const;

const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });

async function readYield(
  walletAddress: `0x${string}`,
  synthetic: `0x${string}` = WUSD, // the wallet's active asset (WUSD or WEUR)
) {
  // Current APR — uint32 in basis points (÷100 for %). Flexible, read live.
  const apr = await publicClient.readContract({
    address: YIELD_CALCULATOR, abi: YieldCalculatorAbi,
    functionName: 'getCurrentApr', args: [synthetic],
  });

  // Pending (claimable) yield for the wallet
  const pending = await publicClient.readContract({
    address: synthetic, abi: SyntheticTokenAbi,
    functionName: 'pendingYield', args: [walletAddress],
  });

  // Total yield already claimed
  const claimed = await publicClient.readContract({
    address: synthetic, abi: SyntheticTokenAbi,
    functionName: 'claimedYield', args: [walletAddress],
  });

  console.log('APR:              ', apr, `(${Number(apr) / 100}%)`);
  console.log('Accumulated yield:', pending.accumulatedYield);
  console.log('Last balance:     ', pending.lastBalance);
  console.log('Last update time: ', pending.lastUpdateTime);
  console.log('Total claimed:    ', claimed);
}

readYield('0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE').catch(console.error);

getCurrentApr returns a uint32 in basis points — divide by 100 for the percentage. The rate is flexible, so always read it live rather than hardcoding a value. pendingYield returns:

FieldTypeDescription
accumulatedYielduint256Yield accrued and available to claim
lastBalanceuint256Balance recorded at the last accrual update
lastUpdateTimeuint64Unix timestamp of the last accrual update

Switching and Claiming On-Chain

setActiveYieldToken (switch asset) and claimYield (claim) are state-changing calls submitted as UserOperations from the user's AA wallet. The minimum claimable amount is 1 USD.

This complete script encodes both calls and submits them. It needs a Kernel (ZeroDev) account client — obtain your own, or get one from the SDK with await sdk.crypto.wallet.getSmartWalletClient() (see SDK).

import { encodeFunctionData, type Address, type Hex } from 'viem';

const YieldCalculatorAbi = [
  { type: 'function', name: 'setActiveYieldToken', stateMutability: 'nonpayable',
    inputs: [{ type: 'address', name: 'synthetic' }], outputs: [] },
] as const;

const SyntheticTokenAbi = [
  { type: 'function', name: 'claimYield', stateMutability: 'nonpayable',
    inputs: [], outputs: [{ type: 'uint256' }] },
] as const;

// Sandbox / Base Sepolia. For production, swap in the Mainnet addresses.
const ADDRESSES = {
  yieldCalculator: '0x7D390c3D77792ad0EA4dC0D36B114E49f91B967a' as Address,
  WUSD: '0x0774164DC20524Bb239b39D1DC42573C3E4C6976' as Address,
  WEUR: '0x5c55F314624718019A326F16a62A05D6C6d8C8A2' as Address,
};

type Call = { to: Address; value: bigint; data: Hex };

function buildSetActiveYieldTokenCall(syntheticAsset: Address): Call {
  return {
    to: ADDRESSES.yieldCalculator,
    value: 0n,
    data: encodeFunctionData({
      abi: YieldCalculatorAbi,
      functionName: 'setActiveYieldToken',
      args: [syntheticAsset],
    }),
  };
}

function buildClaimYieldCall(syntheticAsset: Address): Call {
  return {
    to: syntheticAsset,
    value: 0n,
    data: encodeFunctionData({
      abi: SyntheticTokenAbi,
      functionName: 'claimYield',
      args: [],
    }),
  };
}

// Submit calls as one UserOperation and wait for the receipt.
// sendUserOperation() returns the UserOperation hash directly.
async function sendCalls(smartWalletClient: any, calls: Call[]): Promise<Hex> {
  const callData = await smartWalletClient.account.encodeCalls(calls);
  const userOpHash = await smartWalletClient.sendUserOperation({ callData });
  const { receipt } = await smartWalletClient.waitForUserOperationReceipt({
    hash: userOpHash,
  });
  return receipt.transactionHash;
}

// smartWalletClient — a Kernel (ZeroDev) account client. From the SDK use
// `await sdk.crypto.wallet.getSmartWalletClient()`, or pass your own.
async function switchAndClaim(smartWalletClient: any) {
  // Switch the active yield asset to WEUR (use ADDRESSES.WUSD to switch back)
  const switchTx = await sendCalls(smartWalletClient, [
    buildSetActiveYieldTokenCall(ADDRESSES.WEUR),
  ]);
  console.log('Switched active asset, tx:', switchTx);

  // Claim accrued yield on the active asset (WUSD here) — minimum 1 USD
  const claimTx = await sendCalls(smartWalletClient, [
    buildClaimYieldCall(ADDRESSES.WUSD),
  ]);
  console.log('Claimed yield, tx:', claimTx);
}

For the Kernel account ABI and UserOperation setup, see ABI Reference. For the broader two-phase on-chain pattern used by transfers, see Withdrawals.


Did this page help you?