If you’re using Para for embedded wallets, you can now add yield, lending, borrowing, and portfolio management to your app using the Compass API. Your users get a full DeFi experience without ever managing approvals or interacting with protocols directly.
Compass provides a non-custodial DeFi API — you call an endpoint, get back an unsigned payload, and the user signs it via their Para wallet. Users earn yield, borrow against crypto, or rebalance portfolios without touching a DeFi protocol.This walkthrough covers three products: Earn (deposit into Aave, Morpho, Pendle vaults), Credit (borrow stablecoins against collateral), and Portfolio Manager (atomic rebalancing across venues).
Yield Savings Account — Users deposit USDC and earn 4-8% APY across vaults. You embed a 10% performance fee on yield. Users see one balance, one APY.
Crypto-Backed Credit Line — Users borrow USDC against ETH collateral. Display health factor and liquidation risk. Users never interact with Aave directly.
Auto-Rebalancing Portfolio — Monitor rates and rebalance atomically when better opportunities emerge. One signature moves $50K from Aave (4.8%) to Morpho (6.2%).
Want to see what you’d be building? Try Compass Studio — a visual interface for Earn, Credit, and Portfolio products. Explore vaults, simulate transactions, and copy working configurations into your app.
With gas sponsorship, your users never need ETH. You provide a gas sponsor wallet — a server-side private key funded with ETH on your target chain (Base, Ethereum, or Arbitrum). Your backend uses this wallet to pay gas and relay transactions on behalf of users. Compass returns EIP-712 typed data, the user signs it off-chain, and your sponsor wallet submits the transaction.Without gas sponsorship, Compass returns a standard unsigned transaction that the user signs and broadcasts directly from their Para wallet. The user pays their own gas, which requires them to hold ETH.This walkthrough uses gas sponsorship for all examples. See Compass Gas Sponsorship docs for more details.
You can also sponsor gas through Account Abstraction integrations like Alchemy, Pimlico, or ZeroDev instead of running your own sponsor wallet.
Add the Compass TypeScript SDK alongside your existing Para setup. Para’s ParaProvider already includes wagmi — the useSignTypedData hook used for EIP-712 signing is available out of the box.
npm install @compass-labs/api-sdk
Add these server-side environment variables:
# Server-side only — never expose to the clientCOMPASS_API_KEY=your_compass_api_keyGAS_SPONSOR_PK=0x_your_sponsor_wallet_private_key
Compass uses isolated on-chain accounts (Product Accounts) per product. Create a separate account for each product you plan to use — Earn, Credit, or both. Each account is a smart account controlled by the user’s wallet. Compass never holds custody — it orchestrates creation and transaction routing.Each account is deployed once per user. Calling the create endpoint again safely returns the existing address without a new transaction.Your gas sponsor deploys these accounts so the user never needs ETH.
Create Earn Account
Create Credit Account
// app/api/earn-account/create/route.tsimport { CompassApiSDK } from "@compass-labs/api-sdk";import { createWalletClient, createPublicClient, http } from "viem";import { privateKeyToAccount } from "viem/accounts";import { base, mainnet, arbitrum, type Chain } from "viem/chains";const viemChains: Record<string, Chain> = { base, ethereum: mainnet, arbitrum };export async function POST(request: Request) { const { owner, chain } = await request.json(); const sdk = new CompassApiSDK({ apiKeyAuth: process.env.COMPASS_API_KEY, }); const sponsorAccount = privateKeyToAccount( process.env.GAS_SPONSOR_PK as `0x${string}` ); const response = await sdk.earn.earnCreateAccount({ chain, owner, // User's Para wallet address sender: sponsorAccount.address, // Your sponsor pays for deployment estimateGas: true, }); // If account already exists, no transaction is returned if (!response.transaction) { return Response.json({ earnAccountAddress: response.earnAccountAddress }); } const sponsorWallet = createWalletClient({ account: sponsorAccount, chain: viemChains[chain], transport: http(), }); const tx = response.transaction as any; const txHash = await sponsorWallet.sendTransaction({ to: tx.to, data: tx.data, value: BigInt(tx.value || "0"), gas: tx.gas ? BigInt(tx.gas) : undefined, }); await createPublicClient({ chain: viemChains[chain], transport: http() }) .waitForTransactionReceipt({ hash: txHash }); return Response.json({ earnAccountAddress: response.earnAccountAddress, txHash, });}
Before depositing into a vault or borrowing, move tokens from the user’s wallet into their Product Account. This step teaches the three-step signing pattern that every gas-sponsored Compass operation follows.Your backend requests EIP-712 typed data from Compass (prepare), the Para wallet signs it via wagmi’s useSignTypedData (sign), and your backend submits the signed payload through your gas sponsor (execute).
All amounts in the Compass API are in human-readable units (e.g., "100" means 100 USDC), not in base units or wei.
Backend — Prepare
Backend — Execute
Frontend
// app/api/transfer/prepare/route.tsimport { CompassApiSDK } from "@compass-labs/api-sdk";import { privateKeyToAccount } from "viem/accounts";export async function POST(request: Request) { const { owner, chain, token, amount } = await request.json(); const sdk = new CompassApiSDK({ apiKeyAuth: process.env.COMPASS_API_KEY, }); const sponsorAccount = privateKeyToAccount( process.env.GAS_SPONSOR_PK as `0x${string}` ); // Request EIP-712 typed data with gas sponsorship const transfer = await sdk.earn.earnTransfer({ owner, chain, token, amount, action: "DEPOSIT", spender: sponsorAccount.address, gasSponsorship: true, }); // For Credit accounts, use the same pattern: // const transfer = await sdk.credit.creditTransfer({ // owner, chain, token, amount, // action: "DEPOSIT", spender: sponsorAccount.address, // gasSponsorship: true, // }); const eip712 = transfer.eip712!; // Gas-sponsored transfers use Permit2 — returns PermitTransferFrom types. // Product operations (earnManage, creditBorrow) return SafeTx types instead. // Normalize camelCase keys to PascalCase for wagmi. const normalizedTypes = { EIP712Domain: (eip712.types as any).eip712Domain, PermitTransferFrom: (eip712.types as any).permitTransferFrom, TokenPermissions: (eip712.types as any).tokenPermissions, }; return Response.json({ eip712, normalizedTypes, primaryType: eip712.primaryType, domain: eip712.domain, message: eip712.message, });}
// app/api/execute/route.ts — reused by ALL gas-sponsored operationsimport { CompassApiSDK } from "@compass-labs/api-sdk";import { createWalletClient, createPublicClient, http } from "viem";import { privateKeyToAccount } from "viem/accounts";import { base, mainnet, arbitrum, type Chain } from "viem/chains";const viemChains: Record<string, Chain> = { base, ethereum: mainnet, arbitrum };export async function POST(request: Request) { const { owner, eip712, signature, chain } = await request.json(); const sdk = new CompassApiSDK({ apiKeyAuth: process.env.COMPASS_API_KEY, }); // Your gas sponsor wallet const sponsorAccount = privateKeyToAccount( process.env.GAS_SPONSOR_PK as `0x${string}` ); // Compass wraps the user's EIP-712 signature into // a transaction that your sponsor can submit const sponsored = await sdk.gasSponsorship.gasSponsorshipPrepare({ owner, chain, eip712: eip712 as any, signature, sender: sponsorAccount.address, }); // Your sponsor signs and broadcasts — pays the gas const tx = sponsored.transaction as any; const sponsorWallet = createWalletClient({ account: sponsorAccount, chain: viemChains[chain], transport: http(), }); const hash = await sponsorWallet.sendTransaction({ to: tx.to, data: tx.data, value: BigInt(tx.value || "0"), gas: tx.gas ? BigInt(tx.gas) : undefined, maxFeePerGas: BigInt(tx.maxFeePerGas), maxPriorityFeePerGas: BigInt(tx.maxPriorityFeePerGas), }); await createPublicClient({ chain: viemChains[chain], transport: http() }) .waitForTransactionReceipt({ hash }); return Response.json({ success: true, txHash: hash });}
// components/FundAccount.tsximport { useWallet } from "@getpara/react-sdk";import { useSignTypedData, useSwitchChain } from "wagmi";export function FundAccount() { const { data: wallet } = useWallet(); const { signTypedDataAsync } = useSignTypedData(); const { switchChainAsync } = useSwitchChain(); async function fund(token: string, amount: string) { await switchChainAsync({ chainId: 8453 }); // Base // 1. Get EIP-712 typed data from your backend const prepareRes = await fetch("/api/transfer/prepare", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ owner: wallet?.address, chain: "base", token, amount, }), }); const { eip712, normalizedTypes, primaryType, domain, message } = await prepareRes.json(); // 2. Para wallet signs (user sees structured data, not hex) const signature = await signTypedDataAsync({ domain, types: normalizedTypes, primaryType, message, }); // 3. Your backend submits via gas sponsor await fetch("/api/execute", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ owner: wallet?.address, eip712, signature, chain: "base", }), }); } return ( <button onClick={() => fund("USDC", "100")}> Fund Earn Account with 100 USDC </button> );}
Every Compass operation — deposit, withdraw, swap, borrow, repay, bundle — follows this same three-step pattern. Only the SDK call in the Prepare step changes. The Execute route is reused by all operations.
Before your first gas-sponsored transfer, each token needs a one-time Permit2 approval. Without it, earnTransfer and creditTransfer will return an “Insufficient allowance” error.
// app/api/approve-transfer/route.tsimport { CompassApiSDK } from "@compass-labs/api-sdk";export async function POST(request: Request) { const { owner, chain, token } = await request.json(); const sdk = new CompassApiSDK({ apiKeyAuth: process.env.COMPASS_API_KEY, }); const approval = await sdk.gasSponsorship.gasSponsorshipApproveTransfer({ owner, chain, token, gasSponsorship: true, }); const eip712 = approval.eip712; // If no EIP-712 data, the token is already approved if (!eip712) { return Response.json({ approved: true }); } const normalizedTypes = { EIP712Domain: (eip712.types as any).eip712Domain, Permit: (eip712.types as any).permit, }; return Response.json({ eip712, normalizedTypes, primaryType: eip712.primaryType, domain: eip712.domain, message: eip712.message, });}
The frontend signing and execution flow is identical — use the same signTypedDataAsync and /api/execute calls shown above.
Some tokens like USDT and WETH don’t support EIP-2612 permits. For those, set gasSponsorship: false in the approve call — the user signs and pays gas for the one-time approval directly.
The signing pattern is identical across all products. Only the SDK call in the Prepare step changes — swap it in and use the same Execute route and frontend signing flow from Step 3. Product operations return SafeTx types (normalize with EIP712Domain + SafeTx), unlike transfers which use PermitTransferFrom. The backend returns primaryType so the frontend handles both automatically.
Bundle any combination of withdrawals, swaps, and deposits into a single atomic transaction. If any step fails, the entire bundle reverts — no partial state.