An agentic cross-chain yield optimiser built on Tempo. It finds the best USDC yield vaults across EVM chains, builds an allocation plan, and executes deposits. without the user needing gas on any destination chain.
Everything runs on mainnet. Tempo mainnet (chainId 4217), real USDC.e, real vaults on Base and other EVM chains. There is no testnet mode.
Live demo wallets
- Tempo wallet — source of funds on Tempo. most transactions here were signed and submitted by the OmniLayer agent via session key
- EVM wallet on Base — destination EOA receiving vault deposits on Base, executed autonomously by the agent
- EVM wallet on Ethereum — same EOA on Ethereum mainnet
1. cross-chain yield has terrible UX bridging, switching networks, approving tokens, signing transactions across chains. existing optimisers automate execution but don't give users control over what to prioritise — safety, protocol trust, risk tolerance.
2. picking the right vault is harder than it looks sorting by APY is wrong. raw APY gets inflated by short-lived reward emissions and unsustainable incentives. a real allocation weighs base yield, TVL, APY stability over time, routing costs vs position size, and diversification. nobody should have to do this by hand.
- session keys — tempo wallet session keys let OmniLayer execute transactions on behalf of users within spending limits they set at onboarding
- passkeys everywhere — transaction signing and approvals use passkeys. no seed phrases, no browser extensions
- user-controlled autonomy — fully autonomous execution or approve each deposit manually via passkey. you decide how much you trust the agent
- yield intents — users define their preferences (min APY, risk tolerance, preferred protocols, chain targets, tier weights) and the strategy engine builds allocations around them. the agent optimises within what the user actually wants, not just what has the highest number
- deterministic strategy engine — yield scanning, scoring, and capital allocation runs on pure logic. no LLM is involved in financial decisions. Claude / Codex is a conversational interface only
- Node.js 20+
- pnpm
- MongoDB (local or Atlas)
Create a .env at the project root:
MONGODB_URI=mongodb://localhost:27017/omnilayer
LIFI_API_KEY=your_lifi_api_key
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
PORT=3000pnpm install
pnpm dev # development with live reload
pnpm build # compile TypeScript
pnpm start # run compiled outputpnpm test:scanner # vault scan + scoring
pnpm test:strategy # full pipeline: scan → allocate → route → breakeven
pnpm test:e2e # access key registration on Tempo
pnpm test:flash # flash signer / approval flowUsers hold USDC.e on Tempo (chainId 4217) The server holds a delegated secp256k1 access key authorized by the user's passkey. On each agent cycle, the server:
- Scans ~400 yield vaults via the LI.FI Earn API
- Filters, scores, and picks the best vault per tier (fixed / high-yield / safe)
- Gets a bridge + deposit quote from LI.FI
- Signs and submits a transaction on Tempo — one multicall that approves USDC.e and triggers the bridge
- LI.FI's relayer bridges the funds and calls
vault.deposit()on the destination chain - Vault shares land in the user's EVM EOA. no gas needed anywhere except Tempo (paid in USDC.e)
┌─────────────────────────────┐
│ OmniLayer Server │
│ │
User passkey ─────────▶ access key (secp256k1) │
(one-time PKCE setup) │ strategy engine │
│ scheduler (every 5 min) │
└──────┬──────────┬────────────┘
│ │
sign tx │ │ scan vaults
▼ ▼
┌──────────┐ ┌────────────────┐
│ Tempo │ │ LI.FI Earn API │
│ Chain │ │ ~400 vaults │
└────┬─────┘ └────────────────┘
│
USDC.e bridged via lifi
│
┌────▼──────────────────┐
│ LI.FI Relayer │
│ calls vault.deposit()│
│ on destination chain │
└────┬──────────────────┘
│
shares minted to user EOA
│
┌────▼──────────┐
│ User EVM EOA │
│ (Base etc.) │
└───────────────┘
Each run follows this pipeline:
Scheduler tick
│
▼
Guard checks
├─ cooldown elapsed?
├─ evmAddress set?
└─ USDC.e balance > minDeployUsd?
│
▼
Scan vaults (LI.FI Earn API)
│
▼
Hard-gate filter
├─ USDC underlying only (non-Pendle)
├─ TVL floor, APY gates
├─ time lock within limit
└─ chain / protocol allow/block lists
│
▼
Score surviving vaults
├─ safety score (TVL, base APY ratio, stability, lock penalty)
├─ yield score (log-normalised effectiveApy, per-tier)
└─ final score (blended by data completeness)
│
▼
Tiered allocation
├─ fixed_yield (Pendle PTs) 30%
├─ high_yield (APY ≥ 10%) 40%
└─ safe_base (TVL ≥ $5M) 30%
│
▼
For each allocation:
├─ estimate routing cost (POST /advanced/routes)
├─ check breakeven days (cost / daily yield ≤ limit)
└─ fetch execution quote (GET /quote + toContractAddress)
│
▼
Execute
├─ autoExecute=true → sign + submit tx on Tempo
└─ autoExecute=false → create flash signer, notify via Telegram
Every user has a YieldIntent — a set of preferences the strategy engine uses to filter, score, and allocate capital. Users can update it anytime and the next agent cycle picks it up.
interface YieldIntent {
// yield gates
minApyBps: number // minimum APY in basis points (300 = 3%)
minBaseApyPct: number // minimum base (non-reward) APY — filters incentive-only vaults
maxRewardApyRatioPct: number // max % of APY that can come from reward tokens
// safety gates
minTvlUsd: number // minimum vault TVL in USD
maxTimeLockSeconds: number // 0 = liquid only; positive = allow locks up to N seconds
maxApyVolatilityPct: number // filters vaults with erratic APY history
// allocation constraints
maxDeployUsd: number // total capital cap (0 = use full balance)
maxVaults: number // max concurrent positions
maxAllocationPct: number // max % of capital in a single vault
maxBreakevenDays: number // skip if routing cost takes too long to recover via yield
// scope
targetChains: number[] // [] = all chains
preferredProtocols: string[] // score bonus for these
excludedProtocols: string[] // never allocate to these
// tier weights — controls how capital is split across strategies
tierWeightFixed: number // Pendle fixed-yield (default 30)
tierWeightHighYield: number // high APY vaults (default 40)
tierWeightSafe: number // large-TVL safe base (default 30)
}Step 1 — vault discovery via LI.FI Earn API
The agent calls the LI.FI Earn API which returns ~400 vaults with full analytics: APY breakdown (total / base / reward / 1d / 7d / 30d), TVL, underlying tokens, protocol, chain, time lock, and redeemability. This is the raw material for every decision that follows.
Step 2 — hard-gate filter
Vaults are eliminated if they fail any of the user's hard constraints: TVL below minTvlUsd, APY below minApyBps, reward ratio above maxRewardApyRatioPct, wrong chain, excluded protocol, or non-USDC underlying (non-USDC vaults require a DEX swap on destination which fails at small amounts).
Step 3 — scoring
Each surviving vault gets three scores:
effectiveApy = weighted avg of available history (30d × 4 + 7d × 2 + current × 1)
safety_score = avg(tvl_score, base_apy_ratio, apy_stability, time_lock_penalty)
yield_score = log(1 + effectiveApy) / log(1 + max_effectiveApy) [per-tier, 0–1]
final_score = yield_score × completeness + safety_score × (1 − completeness)
Vaults with sparse data lean toward safety score. Well-documented vaults lean toward yield.
Step 4 — tiered allocation
The top vault per tier is selected. Capital is split proportionally across tiers based on the user's tierWeight settings:
| Tier | Criteria | What it gives you |
|---|---|---|
fixed_yield |
Pendle PT vaults | fixed maturity, guaranteed yield |
high_yield |
effectiveApy ≥ 10% | higher upside, some reward-token risk |
safe_base |
TVL ≥ $5M, lower APY | battle-tested liquidity, lower risk |
Step 5 — routing cost check via LI.FI Composer
Before committing to any deposit, the agent calls POST /advanced/routes to estimate the real cost of bridging from Tempo to the destination chain. If routingCost / dailyYield > maxBreakevenDays, the allocation is skipped — it's not worth the fee.
Step 6 — execution via LI.FI Composer
For allocations that pass, the agent calls GET /quote with toContractAddress and toContractCallData set to the vault's deposit(amount, receiver) calldata. LI.FI's relayer bridges the USDC.e and calls the vault deposit atomically on arrival — vault shares land directly in the user's EVM EOA with no destination gas required.
Tempo smart accounts support delegated session keys — a server can be authorized to sign and submit transactions on a user's behalf, scoped to specific spending limits. OmniLayer uses this to execute deposits without requiring the user to be online.
Onboarding (one time)
During registration, the server generates a secp256k1 keypair. The user visits an authUrl and approves the key using their passkey. This produces a KeyAuthorization — a signed delegation that tells the Tempo smart account "this key can spend up to X USDC.e on my behalf". The server stores the private key and the authorization. The user never touches it again.
User passkey
│
▼
signs KeyAuthorization
│
▼
server stores (accessKeyPriv + KeyAuthorization)
│
▼
first transaction includes KeyAuthorization to register the key on-chain
subsequent transactions use the key directly
Every deposit is a two-call multicall on Tempo
[
approve(lifiRouter, amount), // allow LI.FI to spend USDC.e
bridgeTx( // LI.FI bridge + destination deposit
toContractAddress = vault,
toContractCallData = deposit(amount, userEOA)
)
]Submitted via eth_sendRawTransactionSync. Gas is paid in USDC.e via feeToken — no native ETH needed on Tempo.
Path 1 — autonomous execution
When autoExecute = true, the agent signs and submits the multicall immediately after the strategy cycle completes. No user interaction. The session key's spending limit is the safety boundary — the agent cannot spend more than what was authorized at onboarding.
strategy cycle completes
│
▼
sign multicall with session key
│
▼
submit to Tempo via eth_sendRawTransactionSync
│
▼
notify user via Telegram with tx hash
Path 2 — flash signer (manual approval)
When autoExecute = false, the agent does not execute immediately. Instead it creates a flash signer — a short-lived PKCE session scoped to that exact transaction and spend amount. The user receives a Telegram message with an approval link. They click it, approve with their passkey, and the transaction executes. If they don't approve within the expiry window, the request is discarded.
strategy cycle completes
│
▼
create flash signer (scoped spend limit, 1hr expiry)
│
▼
send Telegram approval link to user
│
├─ user approves → sign + submit multicall → notify confirmed
│
└─ user ignores → request expires, nothing executes
This path lets users stay in control while still benefiting from the agent's scanning and strategy logic.
1. Register — starts the PKCE access key flow:
curl -X POST http://localhost:3000/api/register \
-H "Content-Type: application/json" \
-d '{"chainId": 4217}'
# returns { authUrl, code, accessKeyAddress }The user visits authUrl and approves the server key with their Tempo passkey. The server polls in the background and stores the KeyAuthorization once approved.
2. Set EVM destination address — the EOA that receives vault shares:
curl -X PUT http://localhost:3000/api/strategy/:address/evm-address \
-H "Content-Type: application/json" \
-d '{"evmAddress": "0x..."}'3. Run the agent:
curl -X POST http://localhost:3000/api/strategy/:address/runThe scheduler also runs automatically every 5 minutes for all registered users.
| Method | Path | Description |
|---|---|---|
POST |
/api/register |
Start PKCE access key registration |
GET |
/api/register/status/:address |
Poll registration status |
GET |
/api/strategy/:address |
Get yield intent + active positions |
PUT |
/api/strategy/:address/intent |
Update yield preferences |
PUT |
/api/strategy/:address/settings |
Update account settings |
PUT |
/api/strategy/:address/evm-address |
Set EVM destination address |
POST |
/api/strategy/:address/run |
Manually trigger the agent |
GET |
/api/strategy/:address/scan |
Preview vault candidates (no execution) |
GET |
/api/vaults |
Raw vault list from LI.FI Earn API |
GET |
/api/positions |
Token positions on any EVM chain |
GET |
/api/quote |
Bridge quote via LI.FI Composer |
During onboarding the server generates a secp256k1 keypair. The user signs a KeyAuthorization with their passkey (via PKCE), delegating that key to act on their behalf. All subsequent transactions are signed server-side — no user interaction needed per transaction.
Each deposit is a two-call multicall submitted via eth_sendRawTransactionSync:
[approve(lifiRouter, amount), bridgeTx(toContractAddress=vault, toContractCallData=deposit(...))]
Gas is paid in USDC.e via feeToken in prepareTransactionRequest. The user never needs native ETH on Tempo.
For operations requiring explicit user consent (autoExecute=false), a flash signer is created — a short-lived PKCE session scoped to a single spend. The user approves via a Telegram link.
The Earn API returns ~400 vaults with full analytics: APY breakdown (total / base / reward / 1d / 7d / 30d), TVL, underlying tokens, protocol, chain, time lock, and redeemability. The server fetches all vaults with a TVL pre-filter, then applies its own hard gates and scoring client-side.
effectiveApy is a weighted average of available historical data points (30d×4, 7d×2, current×1), capped at 50% to ignore reward spikes. Safety score combines TVL (log scale), base-to-total APY ratio, APY stability vs 7d/30d, and a time lock penalty. Yield score is log-normalised per tier so fixed/high/safe vaults compete against their own peers. Final score blends yield and safety weighted by data completeness — sparse vaults lean toward safety.
The key trick: GET /quote accepts toContractAddress, toContractCallData, and toContractGasLimit as query params. LI.FI encodes these into the Across bridge message. After the bridge settles, the Across relayer calls vault.deposit(amount, userEOA) on the destination chain. The relayer fee covers destination gas — the user's EOA needs zero ETH.
Pendle vaults are exempt from this: their proprietary router can't be called this way, so the underlying token is bridged directly to the EOA.
The system currently handles deposits only. Once capital is deployed into a vault on a destination chain, the vault shares sit in the user's EVM EOA. Exiting a position — withdrawing from the vault and bridging back to Tempo — is not yet implemented and must be done manually by the user.
Planned but not yet built:
- Exit / withdrawal execution (call
vault.redeem()on destination chain and bridge back) - Rebalancing out of underperforming positions
- APY drift detection triggering an exit

