Skip to content

Creating a Robo / AI Advisor

A robo advisor (or AI advisor) is a programmatic agent that creates proposals and executes trades on behalf of clients. From the on-chain program’s perspective, a robo advisor is identical to a human advisor — it’s just a Solana keypair that signs transactions.

┌─────────────────────┐ ┌─────────────────────┐
│ Trading Strategy │ │ Solana Blockchain │
│ (your logic) │────▶│ Beluga Program │
│ │ │ │
│ - Market signals │ │ - Advisory accounts │
│ - Risk management │ │ - Proposals │
│ - Position sizing │ │ - Vault custody │
└─────────────────────┘ └──────────┬────────────┘
│ polls
┌──────────▼────────────┐
│ Valuation Service │
│ │
│ - Trade extraction │
│ - P&L computation │
│ - Performance metrics │
│ - Leaderboard ranking │
└────────────────────────┘

Your robo advisor needs a Solana keypair. This can be a file-based keypair or any signer that implements the Solana Signer interface.

Terminal window
solana-keygen new -o robo-advisor.json
solana address -k robo-advisor.json
# → outputs your advisor pubkey

Fund this keypair with enough SOL for transaction fees.

To encrypt proposals with v2 envelopes (allowing both you and the client to decrypt), derive an x25519 keypair:

import { sha512 } from "@noble/hashes/sha2";
import { x25519 } from "@noble/curves/ed25519";
// For a file-based keypair, sign the derivation message directly
// using ed25519 from the keypair's secret key
const DERIVATION_MESSAGE = new TextEncoder().encode("beluga:x25519:v1");
// Hash + clamp to get x25519 key material
const hash = sha512(signature);
const secretKey = clampScalar(hash.slice(0, 32));
const publicKey = x25519.getPublicKey(secretKey);
function clampScalar(scalar: Uint8Array): Uint8Array {
const clamped = new Uint8Array(scalar);
clamped[0] &= 248;
clamped[31] &= 127;
clamped[31] |= 64;
return clamped;
}

Clients create advisory accounts with your pubkey. Your bot should poll for pending advisory accounts where it is the named advisor:

import { Connection, PublicKey } from "@solana/web3.js";
const PROGRAM_ID = new PublicKey("8A3VegJ9k5mcXr1AeXgGD4Zr6XrAdyWnNiCK1AUTidey");
// Fetch all advisory accounts using getProgramAccounts
// Filter by advisor pubkey at offset 40 (after discriminator + owner)
const accounts = await connection.getProgramAccounts(PROGRAM_ID, {
filters: [
{ dataSize: 126 }, // advisory account size
{ memcmp: { offset: 40, bytes: advisorPubkey.toBase58() } },
],
});
import { acceptAdvisoryAccount } from "./lib/program/client";
for (const account of pendingAccounts) {
await acceptAdvisoryAccount(
wallet,
connection,
account.pubkey,
25, // fee: 25 bps = 0.25%
sendTransaction,
);
}

The core loop of a robo advisor:

import { createProposal } from "./lib/program/client";
import { encryptPayload } from "./lib/program/decrypt";
async function proposeSwap(
advisoryAccount: { address: PublicKey; ownerEncryptionKey: Uint8Array; proposalCount: number },
signal: { inputMint: string; outputMint: string; amount: number; pool: string; rationale: string },
) {
// 1. Build the proposal payload
const payload = {
rationale: signal.rationale,
amount: signal.amount,
other_amount_threshold: computeSlippageThreshold(signal),
amount_specified_is_input: true,
a_to_b: isAToB(signal),
pool: signal.pool,
token_mint_a: signal.inputMint,
token_mint_b: signal.outputMint,
pool_type: "cpmm" as const,
};
// 2. Encrypt with the owner's x25519 public key
const encryptedData = encryptPayload(
payload,
advisoryAccount.ownerEncryptionKey,
advisorX25519Keypair, // v2: both parties can decrypt
);
// 3. Submit on-chain
const txSig = await createProposal(
wallet,
connection,
advisoryAccount.address,
advisoryAccount.proposalCount,
encryptedData,
sendTransaction,
);
return txSig;
}

Poll for approved proposals and execute within the deadline:

import { executeProposal } from "./lib/program/client";
// Find approved proposals for your advisory accounts
const proposals = await connection.getProgramAccounts(PROGRAM_ID, {
filters: [
{ memcmp: { offset: 0, bytes: PROPOSAL_DISCRIMINATOR_B58 } },
{ memcmp: { offset: 40, bytes: advisorPubkey.toBase58() } },
],
});
for (const proposal of approvedProposals) {
const parsed = parseProposal(proposal.account.data);
// Check execution deadline hasn't passed
if (Date.now() / 1000 > parsed.executionDeadline) continue;
await executeProposal(wallet, connection, sendTransaction, {
address: proposal.pubkey.toBase58(),
advisoryAccount: parsed.advisoryAccount,
owner: parsed.owner,
pool: parsed.pool,
tokenMintA: parsed.tokenMintA,
tokenMintB: parsed.tokenMintB,
aToB: parsed.aToB,
});
}

When a proposal is executed, fees are deducted from the input amount before the swap:

swap_amount = input_amount - program_fee - advisor_fee
program_fee = (input_amount * program_fee_bps) / 10_000
advisor_fee = (input_amount * advisor_fee_bps) / 10_000

Fees are transferred to:

  • Program fee: Admin’s ATA for the input token
  • Advisor fee: Your ATA for the input token (created automatically during execution if needed)
ConstraintValue
Max encrypted payload512 bytes
Max advisor fee100 bps (1%)
Min execution window60 seconds
Max execution window24 hours
Proposals per accountUnlimited (sequential proposal_count)
Active proposalsTracked in active_proposals (u16)

The off-chain valuation service automatically tracks your advisor performance. No registration or API calls are needed — it discovers advisors by polling on-chain state.

The valuation service runs a continuous poller that:

  1. Scans advisory accounts — discovers all accounts and their advisors via getProgramAccounts
  2. Tracks proposals — monitors proposal status transitions (pending -> approved -> executed)
  3. Extracts trade data — for executed proposals, fetches the transaction signature and parses pre/post token balances to determine actual swap output
  4. Resolves prices — fetches historical token prices from Raydium API at the time of execution
  5. Computes P&L — calculates realized profit/loss for each trade:
input_value_usd = swap_input_amount * input_price_usd
output_value_usd = swap_output_amount * output_price_usd
pnl_usd = output_value_usd - input_value_usd

Each executed proposal generates a trade record:

FieldDescription
inputAmountRawTotal input before fees
programFeeRawProtocol fee deducted
advisorFeeRawYour fee deducted
swapInputRawActual amount sent to AMM
swapOutputRawAmount received from AMM
inputPriceUsd / outputPriceUsdPrices at execution time
pnlUsdRealized P&L

The service computes hourly snapshots with these metrics:

MetricFormula
Win ratewinning_trades / total_trades
Total returntotal_pnl_usd / total_volume_usd
Sharpe ratiomean_return / stddev_return (requires 5+ trades)
Total volumeSum of input_value_usd across all trades
Total P&LSum of pnl_usd across all trades
Total fees earnedSum of advisor fees in USD
AUMAggregate vault balances for your clients
Client countNumber of active advisory accounts
Valuation scoreComposite score used for leaderboard ranking
Terminal window
# All metrics
curl https://valuation-production-2919.up.railway.app/api/v1/advisors/<your_pubkey>
# Trade history
curl https://valuation-production-2919.up.railway.app/api/v1/advisors/<your_pubkey>/trades
# Time-series snapshots
curl https://valuation-production-2919.up.railway.app/api/v1/advisors/<your_pubkey>/performance
# Leaderboard
curl https://valuation-production-2919.up.railway.app/api/v1/leaderboard
Terminal window
curl -X POST https://valuation-production-2919.up.railway.app/api/v1/advisors/<your_pubkey>/name \
-H "Content-Type: application/json" \
-d '{"displayName": "My Robo Advisor"}'
async function run() {
// 1. Accept any pending invitations
const pendingAccounts = await findPendingAccounts(advisorPubkey);
for (const acct of pendingAccounts) {
await acceptAdvisoryAccount(wallet, connection, acct.pubkey, 25, sendTx);
}
// 2. For each active client, check if we should propose
const activeAccounts = await findActiveAccounts(advisorPubkey);
for (const acct of activeAccounts) {
const signal = await strategy.evaluate(acct);
if (signal) {
await proposeSwap(acct, signal);
}
}
// 3. Execute any approved proposals
const approved = await findApprovedProposals(advisorPubkey);
for (const proposal of approved) {
if (withinDeadline(proposal)) {
await executeProposal(wallet, connection, sendTx, proposal);
}
}
// 4. Clean up terminal proposals to reclaim rent
const terminal = await findTerminalProposals(advisorPubkey);
for (const proposal of terminal) {
await closeProposal(wallet, connection, proposal.advisoryAccount, proposal.address, sendTx);
}
}
// Run every N seconds
setInterval(run, 30_000);