Skip to content

Repository files navigation

OmniLayer

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.

Video demo

Agent run output

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

Problems we're solving

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.


Core features

  • 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

How to run

Prerequisites

  • Node.js 20+
  • pnpm
  • MongoDB (local or Atlas)

Environment

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=3000

Install and start

pnpm install
pnpm dev       # development with live reload
pnpm build     # compile TypeScript
pnpm start     # run compiled output

Tests

pnpm 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 flow

What it does

Users 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:

  1. Scans ~400 yield vaults via the LI.FI Earn API
  2. Filters, scores, and picks the best vault per tier (fixed / high-yield / safe)
  3. Gets a bridge + deposit quote from LI.FI
  4. Signs and submits a transaction on Tempo — one multicall that approves USDC.e and triggers the bridge
  5. LI.FI's relayer bridges the funds and calls vault.deposit() on the destination chain
  6. Vault shares land in the user's EVM EOA. no gas needed anywhere except Tempo (paid in USDC.e)

System architecture

                        ┌─────────────────────────────┐
                        │       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.)  │
                        └───────────────┘

Agent pipeline

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

How it works

Current strategy response

Yield intent

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)
}

From intent to allocation: how LI.FI powers the strategy

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 session keys and transaction execution

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.


Onboarding a user

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/run

The scheduler also runs automatically every 5 minutes for all registered users.


API reference

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

Implementation details

Server-side signing on Tempo

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.

Vault discovery via LI.FI Earn API

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.

Scoring

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.

Gasless destination deposits via LI.FI

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.


Current limitations

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

About

An agentic cross-chain yield optimiser built on Tempo. It finds the best USDC yield vaults across EVM chains

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages