Skip to main content

Off-Chain Services

✅ Ready to Dev

These are off-chain service specifications, not smart contracts. A developer can implement from this document.
Six bots that automate protocol operations. All run on a single VPS, share one RPC connection, and interact with on-chain contracts via signed transactions. No bot can extract funds — each has minimal, scoped permissions.
Not smart contracts. No PDAs, no on-chain state, no program deployment. Each bot is a server-side process with a dedicated wallet. All wallets are registered on-chain with specific roles.

Service Overview

BotContractInstructionFrequencyWallet Role
Pool RebalancerNative DEXgrow_liquidity / compress_liquidityEvery 60s (if deviation > 1%)dex_config.rebalancer
Merkle PublisherYield Distributionpublish_rootEvery 10 minconfig.publish_authority
Revenue CrankOwnership Tokendistribute_revenueEvery hour (if conditions met)Permissionless
Convert & Fund CrankYield Distributionconvert_to_rwtAfter each revenue distributionPermissionless
Yield Claim CrankRWT Engine + DEX + OTclaim_yield + compound_yield + claim_yd_for_treasuryEvery 30 minPermissionless
Nexus ManagerNative DEXnexus_swap, nexus_add/remove_liquidityStrategy-basedLiquidityNexus.manager

Pool Rebalancer

Maintains the Monotonic Ladder geometry of master pools (RWT/USDC, RWT/USDY). Reads on-chain NAV from RWT Engine and — depending on direction of NAV movement — calls grow_liquidity (rising NAV) or compress_liquidity (writedown). Does not touch StandardCurve pools.

How It Works

1

Read NAV

Query RWT Engine on-chain: rwt_vault.nav_book_value. This is the target price for RWT.
2

Read Pool State

For each master pool: query pool_state.last_rebalance_nav_bin, pool_state.active_zone_lower, and bin_array. Compute current reference price: price = (1 + bin_step_bps/10000) ^ last_rebalance_nav_bin.
3

Check Deviation

deviation = (nav_price − ref_price) / ref_price (signed). If abs(deviation) > REBALANCE_THRESHOLD → rebalance needed.
4

Calculate NAV Bin

new_nav_bin = log(nav_price) / log(1 + bin_step_bps/10000)
5

Route: Growth vs Compression

  • If new_nav_bin > pool_state.last_rebalance_nav_bingrowth path. Call native_dex::grow_liquidity(new_nav_bin, ACTIVE_ZONE_WIDTH). DEX pulls USDC from Nexus accumulator, extends active bid wall right, redistributes geometric density around new NAV. Permanent tail untouched. Organic ask untouched.
  • If new_nav_bin < pool_state.last_rebalance_nav_bin (governance writedown) → compression path. Call native_dex::compress_liquidity(new_nav_bin, ACTIVE_ZONE_WIDTH). DEX recenters density on lower NAV. Former active zone above new NAV becomes extended bid. Organic ask above new NAV freezes in place for future recovery.

Configuration

ParameterDefaultDescription
REBALANCE_THRESHOLD0.01 (1%)Min absolute NAV deviation to trigger rebalance
ACTIVE_ZONE_WIDTH40Bins in the geometric active bid wall
GEOMETRIC_R_BPS8500Geometric density ratio (r = 0.85). Controls how tightly capital concentrates at the active bin
CHECK_INTERVAL_SECS60How often to check deviation

On-Chain Interaction

ActionInstructionAccounts
Read NAVrwt_vault (read)
Read binspool_state, bin_array (read)
Grownative_dex::grow_liquidityrebalancer (signer), dex_config, pool_state (mut), bin_array (mut), liquidity_nexus (mut), nexus_usdc_ata (mut), pool_vault_b (mut), token_program
Compressnative_dex::compress_liquidityrebalancer (signer), dex_config, pool_state (mut), bin_array (mut)

Permissions

  • CAN: call grow_liquidity / compress_liquidity on master pools
  • CANNOT: swap, add/remove liquidity, change config, pause pools, extract funds
  • If compromised: at worst can trigger redundant rebalances (compute waste) or pick suboptimal new_nav_bin within ±1 bin rounding. Cannot drain pool or Nexus — both grow_liquidity and compress_liquidity conserve capital by design. Team replaces via update_dex_config(rebalancer: new_wallet).
  • Emergency kill-switch: Authority calls update_dex_config(rebalancer = [0u8;32], ...). This immediately freezes grow_liquidity / compress_liquidity on-chain. The bot does not need to be stopped — its CPI calls will revert with InvalidRebalancer. Restore by setting rebalancer back to a real wallet.

Skip Conditions

  • Pool is paused (pool_state.is_active == false)
  • Pool is not a Monotonic Ladder pool (StandardCurve pools need no rebalancing)
  • Deviation below threshold
  • Nexus accumulator empty AND growth path (waits for next Nexus deposit before growing)
  • Last rebalance < 60 seconds ago (debounce)

Merkle Publisher

Builds merkle trees of OT holder weights off-chain and publishes roots on-chain. This is the core of the yield distribution system — without published roots, holders cannot claim.

How It Works

1

Scan Holders

For each OT project: call getParsedProgramAccounts to get all token accounts for the OT mint. Includes regular wallets, RWT Vault PDA (holds OT as backing), DEX pool PDAs (hold OT in reserves), and OT Treasury PDA. Filter to holders with ≥ $100 total protocol holdings (OT + RWT combined).
2

Calculate Weights

For each eligible holder (including PDAs):
cumulative_amount = distributor.total_funded * holder_ot_balance / total_eligible_supply
Non-eligible holders’ share → ARL OtTreasury PDA ["ot_treasury", arl_ot_mint] as a leaf in the tree (protocol revenue). Note: RWT Vault PDA, DEX pool PDAs are regular OT holders in the tree — they claim via their respective CPI instructions (claim_yield, compound_yield). ARL Treasury claims via claim_yd_for_treasury on the ARL OT instance.
3

Build Tree

Construct merkle tree. Leaf format: sha256(claimant_pubkey_bytes || cumulative_amount_le_bytes).
4

Publish Root

Call yield_distribution::publish_root(merkle_root, max_total_claim). Server wallet signs as publish authority.
5

Store Proofs

Save full tree and proofs to database/file (needed for holder claim UIs and crank bots).

Configuration

ParameterDefaultDescription
PUBLISH_INTERVAL_SECS600 (10 min)How often to rebuild and publish
MIN_HOLDING_USD100Minimum holding to be eligible ($100)
PROOF_STORAGEPath or DB where proofs are stored for claim UI

On-Chain Interaction

ActionInstructionAccounts
Read holdersOT token accounts (read via RPC)
Read distributormerkle_distributor (read)
Publishyield_distribution::publish_rootpublish_authority (signer), config, distributor (mut)

Permissions

  • CAN: call publish_root on any distributor
  • CANNOT: fund, claim, close distributors, update config
  • If compromised: attacker can publish fraudulent roots → drain reward vault via fake claims. Team immediately replaces via update_publish_authority. All publishes visible in RootPublished events.
Highest trust bot. A compromised publish authority can redirect yield to attacker wallets. Secure the keypair and monitor RootPublished events.

Cost

~2.60/monthperOTproject(144publishrootTXs/day×30days× 2.60/month per OT project (144 publish_root TXs/day × 30 days × ~0.0006/TX).

Revenue Crank

Triggers OT revenue distribution when conditions are met. Checks each OT project’s RevenueAccount balance and calls distribute_revenue if the balance is above minimum and cooldown has passed.

How It Works

1

Check Balance

For each OT project: read RevenueAccount ATA balance. If balance < min_distribution_amount ($100), skip.
2

Check Cooldown

Read revenue_config.last_distribution_ts. If now - last_distribution_ts < 604,800 (7 days), skip.
3

Distribute

Call ownership_token::distribute_revenue. Permissionless — any wallet can call.
  1. Protocol fee deducted first: 0.25% (25 bps) of total balance → areal_fee_destination (Areal Finance USDC ATA)
  2. Remainder split proportionally to configured destinations (default):
    • 70% (7,000 bps) → YD Accumulator (USDC)
    • 20% (2,000 bps) → OT Treasury
    • 10% (1,000 bps) → Crank USDC ATA → Nexus via nexus_deposit
Example: 1,000revenue1,000 revenue → 2.50 Areal fee → 997.50remainder997.50 remainder → 698.25 YD / 199.50Treasury/199.50 Treasury / 99.75 Nexus

Configuration

ParameterDefaultDescription
CHECK_INTERVAL_SECS3600 (1 hour)How often to check each OT project
OT_PROJECTSList of OT mint addresses to monitor

On-Chain Interaction

ActionInstructionAccounts
Read balanceRevenueAccount ATA (read)
Read cooldownrevenue_config (read)
Distributeownership_token::distribute_revenuecrank (signer), ot_config, revenue_config (mut), revenue_ata (mut), destination ATAs (mut)

Permissions

  • Permissionless — no special wallet role needed. Any wallet can trigger distribution.
  • If compromised: no risk — crank only triggers distribution to pre-configured on-chain destinations. Cannot change destinations or extract funds.

Convert & Fund Crank

Converts accumulated USDC in YD Accumulator PDAs to RWT and deposits into distributor reward vaults. Should run immediately after each revenue distribution to minimize time between USDC arrival and RWT availability for claims.

How It Works

1

Check Accumulator

For each OT project: read Accumulator USDC ATA balance. If 0, skip.
2

Convert

Call yield_distribution::convert_to_rwt(max_swap_amount). Atomic instruction that:
  1. Swaps USDC → RWT on DEX (up to NAV price)
  2. Mints remainder at NAV via RWT Engine
  3. Deducts 0.25% protocol fee in RWT
  4. Deposits net RWT into reward vault
  5. Updates vesting state (locks previously vested, starts new vesting)

Configuration

ParameterDefaultDescription
CHECK_INTERVAL_SECS300 (5 min)How often to check accumulators
MAX_SWAP_RATIO0.5Max fraction of USDC to swap on DEX (rest minted at NAV)

On-Chain Interaction

ActionInstructionAccounts
Read balanceAccumulator USDC ATA (read)
Convert + fundyield_distribution::convert_to_rwtcrank (signer), config, distributor (mut), accumulator, ATAs, DEX accounts, RWT Engine accounts

Permissions

  • Permissionless — any wallet can call. Accumulator PDA signs all internal transfers.
  • If compromised: no risk — instruction only converts USDC already in Accumulator to RWT in reward vault. Cannot redirect funds.

Trigger

Ideally triggered immediately after Revenue Crank distributes USDC to an Accumulator. Can poll on interval as fallback.

Yield Claim Crank

Claims RWT yield from YD merkle streams on behalf of three types of on-chain PDAs:
  1. RWT Vault PDA — holds OT positions, earns yield as OT holder. Claimed RWT split: 70% NAV / 15% Nexus / 15% ARL Treasury.
  2. DEX Pool PDAs — OT/RWT pools hold OT in reserves, earning yield. Claimed RWT auto-compounds into pool reserves, benefiting all LP holders.
  3. OT Treasury PDAs — non-eligible holders’ (< $100) yield share is allocated to OT Treasury PDA in the merkle tree. Claimed RWT stays in treasury.

How It Works

1

Get Proofs

Read the latest merkle proofs from the Merkle Publisher’s proof storage for:
  • RWT Vault PDA (one per protocol)
  • Each OT/RWT pool PDA that holds OT (one per pool)
2

Claim for RWT Vault

Call rwt_engine::claim_yield(cumulative_amount, proof). Permissionless instruction that:
  1. CPIs to yield_distribution::claim with vault PDA as claimant
  2. Splits claimed RWT: 70% stays in vault (NAV growth), 15% → Yield Distribution LiquidityHolding PDA RWT ATA, 15% → ARL Treasury
  3. Updates total_invested_capital and recalculates NAV
  4. Authority subsequently drains the LiquidityHolding slice into the Liquidity Nexus via yield_distribution::withdraw_liquidity_holding (single TX, atomic principal-floor update via CPI to native_dex::nexus_record_deposit). The crank does not handle the RWT lane — it is Authority-gated
3

Compound for DEX Pools

For each OT/RWT pool: call native_dex::compound_yield(cumulative_amount, proof). Permissionless instruction that:
  1. CPIs to yield_distribution::claim with pool PDA as claimant
  2. Claimed RWT added directly to pool reserves (auto-compound)
  3. All LP holders benefit proportionally — no individual claim needed
4

Claim for ARL Treasury (non-eligible yield)

Call ownership_token::claim_yd_for_treasury(cumulative_amount, proof) on the ARL OT instance. This claims non-eligible holders’ yield from ALL OT projects as protocol revenue:
  1. CPIs to yield_distribution::claim with ARL OtTreasury PDA as claimant
  2. Claimed RWT arrives in ARL Treasury’s RWT ATA
  3. RWT is protocol revenue — spendable by ARL Futarchy governance via spend_treasury
Called once per distributor (one per OT project) — each distributor has a separate merkle tree with ARL Treasury as a leaf for that project’s non-eligible share.

Configuration

ParameterDefaultDescription
CLAIM_INTERVAL_SECS1800 (30 min)How often to attempt claims
PROOF_SOURCEMerkle Publisher’s proof storage (DB or API)
OT_RWT_POOLSList of OT/RWT pool addresses to compound
SEND_TXfalseDry-run flag. true = live submit; false = compute and log decisions only. Flipped to true after the staging-mode run is verified.

On-Chain Interaction

ActionInstructionAccounts
Read proofsOff-chain: Merkle Publisher proof storage
Claim for vaultrwt_engine::claim_yieldcrank (signer, mut), rwt_vault (mut), dist_config, RWT ATAs, YD CPI accounts
Compound for poolnative_dex::compound_yieldcrank (signer, mut), pool_state (mut), target_vault (mut), YD CPI accounts
Claim for treasuryownership_token::claim_yd_for_treasurycrank (signer, mut), ot_treasury, treasury_rwt_ata, YD CPI accounts
Route USDC to Nexusnative_dex::nexus_depositcrank (signer), nexus (mut), token accounts. USDC lane only — the RWT lane is Authority-gated via withdraw_liquidity_holding and is not driven by this crank.

Permissions

  • Permissionless — any wallet can call all three instructions. PDAs sign their own CPI claims internally.
  • If compromised: no risk — claimed RWT goes to pre-configured on-chain destinations. Cannot change split ratios, destinations, or redirect pool compound.

Dependency

Requires Merkle Publisher to have published a root that includes RWT Vault PDA and pool PDAs as claimants. If no valid proof exists, claim will fail with InvalidProof.

Nexus Manager

Manages the Liquidity Nexus PDA’s positions in DEX pools. The Nexus holds protocol-owned liquidity (funded by 10% of OT revenue + 15% of RWT yield). The manager bot decides when and where to deploy this capital. Principal is permanently locked — only LP profits (swap fees, auto-compound) can be withdrawn to Areal Treasury by authority.

How It Works

1

Deposit Capital

Two lanes route capital into the Nexus with principal tracking — neither is driven by the Nexus Manager itself, but both feed the capital that the Manager subsequently deploys:
  • USDC lane. OT distribute_revenue sends 10% USDC to a crank wallet. The crank calls native_dex::nexus_deposit to route the USDC into the Nexus and bump total_deposited_usdc.
  • RWT lane. RWT claim_yield sends the 15% liquidity slice into the Yield Distribution LiquidityHolding PDA’s RWT ATA. Authority then calls yield_distribution::withdraw_liquidity_holding, which atomically transfers RWT from LiquidityHolding into the Nexus RWT ATA AND CPIs into native_dex::nexus_record_deposit to update total_deposited_rwt — single TX.
2

Monitor Nexus Balance

Read Liquidity Nexus RWT and USDC balances. Check for undeployed capital.
3

Evaluate Pools

Analyze DEX pools: TVL, volume, spread, utilization. Identify pools that need more liquidity.
4

Deploy Liquidity

Call native_dex::nexus_add_liquidity to LP into target pools. Can also call nexus_swap to rebalance between tokens before adding.
5

Rebalance

Periodically rebalance positions: nexus_remove_liquidity from underperforming pools, nexus_add_liquidity to better pools.
6

Withdraw Profits

Authority (Team Multisig) periodically calls nexus_withdraw_profits to send LP profits to Areal Treasury. Only amount above principal is withdrawable. Manager must nexus_remove_liquidity first to bring tokens back to ATAs.

Configuration

ParameterDefaultDescription
CHECK_INTERVAL_SECS300 (5 min)How often to evaluate positions
MIN_DEPLOY_AMOUNT1,000,000 ($1 RWT)Minimum amount to deploy in one operation
MAX_POOL_CONCENTRATION0.5 (50%)Max fraction of Nexus capital in one pool

On-Chain Interaction

ActionInstructionAccounts
Read balanceNexus ATAs (read)
Swapnative_dex::nexus_swapnexus_manager (signer), dex_config, nexus, pool accounts
Add LPnative_dex::nexus_add_liquiditynexus_manager (signer), dex_config, nexus, pool accounts
Remove LPnative_dex::nexus_remove_liquiditynexus_manager (signer), dex_config, nexus, pool accounts
Withdraw profitsnative_dex::nexus_withdraw_profitsauthority (signer), dex_config, nexus (mut), token accounts

Permissions

  • CAN: swap, add/remove liquidity using Nexus PDA’s funds
  • CANNOT: withdraw principal, change DEX config, pause pools
  • If compromised: attacker can make bad trades with Nexus funds (LP into bad pools, swap at bad prices), but cannot extract principal — protected on-chain (ata_balance - withdrawal >= principal). Team replaces via update_nexus_manager.
Manager trust: Nexus Manager has discretion over ~25% of protocol capital flow (10% OT revenue + 15% RWT yield). Monitor positions and P&L. In V2, this could become an AI agent with automated strategy.

Operational hardening

The Nexus Manager bot uses the shared operational primitives that other privileged Areal bots rely on:
  • Multi-RPC fallback. Reads and submits go through a multi-endpoint RPC client; security-critical state (the Nexus principal floor, pool reserve depth before a swap) is read with a consensus check across endpoints to defend against single-RPC fault or stale-state attacks.
  • Single-instance lock. A PID-file lock at startup prevents two manager instances from racing on the same RPC and the same managed-pool list. A second instance exits immediately with a structured error.
  • WS reconcile after disconnect. On a WebSocket reconnect, the bot walks program signatures since the last-seen slot to backfill missed NexusDeposited and NexusManagerUpdated events.
  • SOL pre-flight check. The bot asserts the manager wallet has enough SOL before the first submit each cycle; if the wallet is dry, the cycle returns a structured skip with reason low_sol rather than failing inside the transaction send call.
  • Kill-switch authority. When the Nexus Manager is suspected compromised, Authority (Team Multisig) calls update_nexus_manager(new_manager: [0u8; 32]). The three manager-only DEX instructions (nexus_swap, nexus_add_liquidity, nexus_remove_liquidity) all check signer != [0u8; 32] and revert with NexusManagerDisabled. Operations stay paused until Authority calls update_nexus_manager again with a real pubkey.
  • Decision engine. The bot ranks managed pools by drift from a target distribution (default: equal weight across configured pools) and emits one of four decision kinds per cycle: swap, add_liquidity, remove_liquidity, or skip with a structured reason. Decisions are persisted append-only on disk in JSONL for after-the-fact review.
  • SEND_TX flag — dry-run mode. The bot defaults to SEND_TX=false (compute and log decisions, but do not submit). Operators flip SEND_TX=true in the production environment after a staging-mode run is verified. Dry-run is the canonical staging-test before live submit.

Shared Infrastructure

All six bots run on a single VPS with shared configuration:

Architecture

┌──────────────────────────────────────────────────────┐
│                    VPS ($10/month)                    │
│                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │   Pool       │  │   Merkle     │  │  Revenue   │ │
│  │  Rebalancer  │  │  Publisher   │  │   Crank    │ │
│  │  (60s loop)  │  │  (10min loop)│  │  (1h loop) │ │
│  └──────┬───────┘  └──────┬───────┘  └──────┬─────┘ │
│         │                 │                 │        │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │  Convert &   │  │  Yield Claim │  │   Nexus    │ │
│  │  Fund Crank  │  │    Crank     │  │  Manager   │ │
│  │  (5min loop) │  │  (30min loop)│  │  (5min)    │ │
│  └──────┬───────┘  └──────┬───────┘  └──────┬─────┘ │
│         │                 │                 │        │
│         └────────────┬────┴────────┬────────┘        │
│                      ▼             ▼                  │
│              ┌──────────────┐ ┌──────────┐           │
│              │  Shared RPC  │ │ Wallets  │           │
│              │  (Helius)    │ │  (.keys) │           │
│              └──────┬───────┘ └────┬─────┘           │
└─────────────────────┼──────────────┼─────────────────┘
                      │              │
                      ▼              ▼
              ┌─────────────────────────┐
              │    Solana Blockchain    │
              └─────────────────────────┘

Wallets

BotWallet TypeRegistered As
Pool RebalancerDedicated keypairdex_config.rebalancer
Merkle PublisherDedicated keypairconfig.publish_authority
Revenue CrankShared crank walletPermissionless (no registration)
Convert & FundShared crank walletPermissionless (no registration)
Yield ClaimShared crank walletPermissionless (no registration)
Nexus ManagerDedicated keypairLiquidityNexus.manager
Three permissionless bots can share one wallet. Three privileged bots need dedicated keypairs.

Environment Variables

VariableDescription
RPC_URLSolana RPC endpoint (Helius free tier sufficient)
REBALANCER_KEYPAIRPool Rebalancer wallet keypair
PUBLISHER_KEYPAIRMerkle Publisher wallet keypair
NEXUS_MANAGER_KEYPAIRNexus Manager wallet keypair
CRANK_KEYPAIRShared wallet for permissionless operations
RWT_VAULT_ADDRESSRWT Engine vault PDA
CONCENTRATED_POOLSList of concentrated pool addresses (restart required on change)
OT_PROJECTSList of {ot_mint, distributor, accumulator, revenue_config} per project (restart required on change)

Cost Estimate

ComponentCost/month
VPS$10
Solana TX fees (~500 TXs total)~$0.30
RPC (Helius free tier)$0
Total~$10/month

Operational Requirements

RequirementDetail
InfrastructureVPS, always-on, systemd or Docker
SOL balanceEach wallet: ~0.1 SOL for TX fees
MonitoringLog every TX with bot name, instruction, result
AlertingAlert if any bot fails for > 15 minutes
Key managementKeypairs encrypted at rest, never committed to git
Restart policyAuto-restart on crash, exponential backoff

Event-Driven Optimizations

Instead of purely interval-based polling, bots can subscribe to Solana events for faster reactions:
EventTriggers
CapitalAdjusted (RWT Engine)Pool Rebalancer checks immediately (NAV changed)
Revenue SPL transfer to RevenueAccountRevenue Crank checks immediately
StreamConverted (YD)Merkle Publisher publishes new root (total_funded changed)
RootPublished (YD)Yield Claim Crank claims immediately (new proof available)

Deployment Order

  1. Deploy all smart contracts
  2. Bootstrap: initialize_* all contracts, set authority, register wallets
  3. Register bot wallets on-chain:
    • update_dex_config(rebalancer: rebalancer_pubkey) — Pool Rebalancer
    • initialize_nexus(manager: manager_pubkey) — Nexus Manager (first-time singleton creation; subsequent rotation via update_nexus_manager(new_manager); the manager pubkey lives on LiquidityNexus.manager)
    • Publish authority set at initialize_config — Merkle Publisher
  4. Fund bot wallets with ~0.1 SOL each
  5. Start all services, verify logs
  6. Transfer authorities to Team Multisig

Trust Summary

BotTrust LevelIf Compromised
Pool RebalancerLowCan shift bins to bad positions. No fund extraction. Replace wallet.
Merkle PublisherHighCan publish fake roots → drain reward vaults. Replace immediately + monitor.
Revenue CrankNonePermissionless. Can only trigger to preset destinations.
Convert & FundNonePermissionless. Converts to preset vault.
Yield ClaimNonePermissionless. Claims to preset destinations.
Nexus ManagerMediumCan make bad trades with Nexus funds. Cannot extract. Replace wallet.