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.
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
{
"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
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:
- System cron on a tiny VPS:
0 9 * * * /usr/bin/node /opt/sera-rebalance/rebalance.js - GitHub Actions with a scheduled workflow — clean, free for low-frequency, audit-trail in PRs
- A serverless cron (Vercel cron, Cloudflare Workers cron) — only viable if your sera-mcp is hosted as a service, not spawned per call
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
- "No action" is the right action most days. Set a meaningful drift threshold (e.g. 5 percentage points) — daily rebalances on tiny drift just burn gas.
- Reporting currency matters. If you report in USD but your business is JPY-denominated, the drift signals will mislead. Use the operational reporting currency.
- Sera finds the plan; it doesn't choose when. You might want to delay execution until you see a favorable rate via
limit_watcher.
Next: x402-paid API tutorial — monetize your rebalancer's output as a paid signal feed.