Sera Sera for Agents / Docs / Tutorials / Treasury rebalancer
Tutorial 04

Treasury rebalancer.

A cron job that reads multi-currency wallet balances, computes drift from target weights, and emits Sera swap intents. Runs daily, signs nothing — emits a plan you (or a multi-sig flow) execute.

20 min Node CLI cron / GitHub Actions rebalance_plan
What you'll have at the end

A script that runs daily, compares your treasury composition to a target (e.g. 50% USDC / 30% EURC / 20% XSGD), and drops a swap plan into Slack/Telegram for approval. Optional auto-execution behind a multi-sig.

Steps

1Define your targets

config/targets.json
{
  "wallets": [
    "0xAa…operating",
    "0xBb…reserve"
  ],
  "targets": { "USDC": 0.5, "EURC": 0.3, "XSGD": 0.2 },
  "reporting_currency": "USD",
  "drift_threshold_pct": 5,
  "alert_webhook": "https://hooks.slack.com/…"
}

2The rebalancer script

rebalance.ts
import { spawnSeraMcp } from './mcpClient';
import targets from './config/targets.json';

const sera = spawnSeraMcp({ env: { SERA_NETWORK: 'mainnet' } });

async function main() {
  // 1. Snapshot exposure
  const exposure = await sera.call('exposure_report', {
    wallets: targets.wallets, reporting_currency: targets.reporting_currency
  });

  // 2. Compute drift vs targets
  const drift = Object.entries(targets.targets).map(([sym, t]) => ({
    symbol: sym,
    target: t,
    actual: exposure.weights[sym] ?? 0,
    drift_pp: ((exposure.weights[sym] ?? 0) - t) * 100
  }));

  const maxDrift = Math.max(...drift.map(d => Math.abs(d.drift_pp)));
  if (maxDrift < targets.drift_threshold_pct) {
    console.log(`No action — max drift ${maxDrift.toFixed(1)}pp under threshold`);
    return;
  }

  // 3. Get the swap plan
  const plan = await sera.call('rebalance_plan', {
    wallets: targets.wallets,
    targets: targets.targets,
    reporting_currency: targets.reporting_currency
  });

  // 4. Post to Slack/Telegram for approval
  await postPlan(plan, drift);
}

async function postPlan(plan, drift) {
  const lines = [
    `*Treasury drift*`,
    ...drift.map(d => `• ${d.symbol}: actual ${(d.actual*100).toFixed(1)}% vs target ${(d.target*100)}% (${d.drift_pp>0?'+':''}${d.drift_pp.toFixed(1)}pp)`),
    `*Suggested swaps*`,
    ...plan.swaps.map(s => `• ${s.amount} ${s.from} → ${s.to}`)
  ];
  await fetch(targets.alert_webhook, {
    method: 'POST',
    body: JSON.stringify({ text: lines.join('\n') })
  });
}

main().catch(console.error);

3Schedule it

Three good options:

.github/workflows/rebalance.yml
name: rebalance
on:
  schedule:
    - cron: '0 9 * * *'     # 09:00 UTC daily
  workflow_dispatch: {}    # manual trigger button
jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: node rebalance.js
        env:
          SERA_NETWORK: mainnet
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

4(Optional) Auto-execute behind a multi-sig

If your treasury is held in a Safe (Gnosis), the rebalancer can stage transactions instead of just posting plans. Use Safe's Transaction Service API to submit each swap from the plan as a pending Safe tx, awaiting signer approvals.

For each swap in plan.swaps: call sera.get_quote, build a multicall (approve + execute), submit to Safe.

Common gotchas

Next: x402-paid API tutorial — monetize your rebalancer's output as a paid signal feed.