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:
| Guide | Why |
|---|---|
| Getting Started | Platform overview and setup |
| Api Basics | Required headers and request configuration |
| Onboarding | User and wallet registration |
| Unified Balance | WUSD/WEUR token mechanics |
| ABI Reference | Smart 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
YieldCalculatorcontract. - 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:
| Contract | Role |
|---|---|
YieldCalculator | Tracks the active yield asset per wallet and computes APR |
WUSD / WEUR (unified token) | Holds the balance, accrues yield, and processes claims |
The flow:
- Wallet is created and funded with WUSD/WEUR.
- Yield accrues on the active asset from that moment (WUSD by default).
- Optionally, switch the active asset to WEUR via
YieldCalculator.setActiveYieldToken(). - Read pending yield at any time; claim it when it reaches the minimum.
- 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
| Method | Returns | Description |
|---|---|---|
fetchYieldData() | { apy, currentPendingYield, weeklyYield, lifeTimeYield } | APY (%) plus pending, weekly, and lifetime yield |
getWeeklyYield() | string | Projected yield over the next 7 days |
getYieldDataForGraphic() | { timestamp, yield }[] | Time series for charting (see note below) |
switchYieldToken(token) | void | Switch the active asset; pass a TokenInfo from sdk.crypto.getToken() |
withdraw() | void | Claim 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)
| Contract | Address |
|---|---|
| YieldCalculator | 0xEd8424728A90A11b7531422AbD9FF7bbB9bEB562 |
| WUSD | 0xb4bB2032A73A53C0Aa7Dc9ee2d9658a978fA7bC2 |
| WEUR | 0x379e120C1921bFD8f5E0A1a3C699e7e800b66606 |
Sandbox (Base Sepolia — Chain ID 84532)
| Contract | Address |
|---|---|
| YieldCalculator | 0x7D390c3D77792ad0EA4dC0D36B114E49f91B967a |
| WUSD | 0x0774164DC20524Bb239b39D1DC42573C3E4C6976 |
| WEUR | 0x5c55F314624718019A326F16a62A05D6C6d8C8A2 |
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:
| Field | Type | Description |
|---|---|---|
accumulatedYield | uint256 | Yield accrued and available to claim |
lastBalance | uint256 | Balance recorded at the last accrual update |
lastUpdateTime | uint64 | Unix 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.
Updated 20 days ago

