Project: Build a Dual-Control IMM Swap Desk
A complete, runnable project: a small Node.js service that stands up an IMM market between two assets, lets users swap against it, and puts real operational guardrails around it — a dedicated tenant-owned signing wallet (Wallet Config) instead of the shared relayer, two-person sign-off before real money moves (Approvals), and a Risk dashboard endpoint so you can see exposure and VaR instead of guessing. This is every piece from those reference sections, wired together into one thing you build and run rather than isolated API calls.
What you'll build
A swap-desk service with three routes of its own — POST /vault/setup, POST /swap, GET /status — that internally drive comet-engine. By the end, KES → IMC (or KES → CUSD on Stellar) swaps run through your own desk, backed by your own wallet, and funding that wallet requires two approvers before it actually happens. Pick a chain above — every script below follows that choice, same as the API reference pages.
Prerequisites
- Node.js 18+ (for global
fetch) - A running comet-engine (
cd comet-engine && go run ./cmd/engine) with a tenant API key/secret — see Authentication - Two wallets you control as your two approvers — Metamask (or any EVM wallet) for Celo, or two Stellar keypairs (e.g. via
stellar-sdk'sKeypair.random()) for Stellar. Neither is something comet-engine generates for you.
Project setup
mkdir swap-desk && cd swap-desk
npm init -yCreate .env:
COMET_BASE_URL=http://localhost:8100
COMET_API_KEY=your-tenant-api-key
COMET_API_SECRET=your-tenant-api-secret
CHAIN=celo
QUOTE_ASSET=IMC
APPROVER_1_ADDRESS=0xYourFirstApproverAddress
APPROVER_2_ADDRESS=0xYourSecondApproverAddressCreate comet.js — a tiny fetch wrapper every route below reuses:
// comet.js
import 'dotenv/config';
const base = process.env.COMET_BASE_URL;
const headers = {
'Content-Type': 'application/json',
'X-API-Key': process.env.COMET_API_KEY,
'X-API-Secret': process.env.COMET_API_SECRET,
};
export async function comet(method, path, body) {
const res = await fetch(base + path, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (!res.ok) throw new Error(`${method} ${path} -> ${res.status}: ${JSON.stringify(data)}`);
return data;
}Step 1: Set the rate and register your own vault wallet
Your desk mints its quote asset against KES at a rate you set — and instead of that mint signing with comet-engine's shared relayer, it uses a wallet dedicated to your desk. See Wallet Config for the generate-vs-import distinction and what "feature" scoping means. The only thing that changes per chain is the signer scheme on the approval policy — everything else reads CHAIN/QUOTE_ASSET from .env:
// setup.js
import { comet } from './comet.js';
const scheme = process.env.CHAIN === 'stellar' ? 'stellar' : 'evm';
async function main() {
await comet('POST', '/api/v1/admin/imm/rates', { base: 'KES', quote: process.env.QUOTE_ASSET, rate: 1.0 });
console.log(`rate set: 1 KES = 1 ${process.env.QUOTE_ASSET}`);
const wallet = await comet('POST', '/api/v1/admin/wallet-config/relayer/generate', {
chain: process.env.CHAIN,
feature: 'imm.vault',
scheme,
});
console.log('dedicated vault wallet:', wallet.wallet.address);
// Two-person sign-off before this wallet can be funded, rebalanced, or
// have its rate changed — see /api/imm#approvals-dual-control for the full mechanics.
const policy = await comet('POST', '/api/v1/admin/approvals/policies', {
actionScope: 'imm.*',
signers: [
{ label: 'Ops', address: process.env.APPROVER_1_ADDRESS, scheme },
{ label: 'Finance', address: process.env.APPROVER_2_ADDRESS, scheme },
],
threshold: 2,
expiryMinutes: 60,
});
console.log('dual-control policy active for every imm.* action');
}
main().catch((e) => { console.error(e); process.exit(1); });npm install dotenv
node setup.jsStep 2: Fund the vault (and approve it)
curl -X POST "http://localhost:8100/api/v1/admin/imm/vault/fund" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-API-Secret: YOUR_API_SECRET" \
-d '{
"chain": "celo",
"asset": "IMC",
"amount": 5000,
"externalId": "vault-seed-001"
}'{
"status": "pending_approval",
"approvalId": "b3f1...",
"actionScope": "imm.vault.fund",
"threshold": 2,
"expiresAt": "2026-09-10T15:00:00.000Z"
}Because Step 1 set a policy on imm.*, this doesn't fund anything yet — your two approvers each sign the pending action's canonical message with their own wallet and post it:
// approve.js — run once per approver, with that approver's own wallet
import { Wallet } from 'ethers'; // npm install ethers
import { comet } from './comet.js';
const approvalId = process.argv[2]; // from Step 2's response
const canonicalMessage = process.argv[3]; // "comet-engine-approval:v1:<id>:<scope>:<payloadHash>"
const privateKey = process.env.APPROVER_PRIVATE_KEY;
async function main() {
const wallet = new Wallet(privateKey);
const signature = await wallet.signMessage(canonicalMessage); // EIP-191 personal_sign
const result = await comet('POST', `/api/v1/admin/approvals/${approvalId}/approve`, {
scheme: 'evm', address: wallet.address, signature,
});
console.log(result.pending.status); // "pending" after the 1st approver, "executed" after the 2nd
}
main();Run it with each approver's own key — the second call actually funds the vault and returns the real txHash.
Step 3: Run swaps against your desk
curl -X GET "http://localhost:8100/api/v1/imm/quote?base=KES"e=IMC&amountIn=500" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-API-Secret: YOUR_API_SECRET"{
"base": "KES",
"quote": "IMC",
"rate": 1,
"amountIn": 500,
"amountOut": 500,
"rateAge": "2m10s"
}curl -X POST "http://localhost:8100/api/v1/imm/swap" \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-API-Secret: YOUR_API_SECRET" \
-d '{
"externalUserId": "user-24",
"chain": "celo",
"base": "KES",
"quote": "IMC",
"amountIn": 500,
"externalId": "desk-swap-001"
}'{
"status": "success",
"chain": "celo",
"result": {
"amountOut": 500,
"baseTxHash": "0x...",
"quoteTxHash": "0x...",
"vaultBacked": true
}
}Regular swaps aren't gated by the imm.* policy you set — only the admin actions that move or reprice the vault itself are (Approvals lists exactly which scopes those are). If the quote leg ever fails after the base leg lands, the engine automatically reverses the base transfer before returning an error — your user's balance is never left half-charged.
Step 4: Check what the desk actually looks like
// status.js — CHAIN and QUOTE_ASSET come from .env, so this is unchanged across chains
import { comet } from './comet.js';
const chain = process.env.CHAIN;
const asset = process.env.QUOTE_ASSET;
async function main() {
const exposure = await comet('GET', `/api/v1/admin/imm/exposure?chain=${chain}&asset=${asset}`);
console.log('net exposure:', exposure.netPosition, '/ cap', exposure.exposureCap);
const varEstimate = await comet('GET', `/api/v1/admin/risk/var?chain=${chain}&asset=${asset}&confidence=0.95`);
console.log('95% VaR:', varEstimate);
const pnl = await comet('GET', `/api/v1/admin/risk/pnl?chain=${chain}&asset=${asset}&sinceDays=7`);
console.log('7-day PnL rollup:', pnl);
}
main().catch((e) => console.error(e.message)); // VaR needs ~10 recorded snapshots (1/hour) before it returns dataAutomatic rebalancing, opt-in
If you want the desk to flatten its own exposure automatically once it crosses a threshold instead of waiting for someone to notice, set IMM_REBALANCE_TARGET_<QUOTE_ASSET> (e.g. IMM_REBALANCE_TARGET_IMC=USDT on Celo, IMM_REBALANCE_TARGET_CUSD=CTEST on Stellar) on the engine — see IMM & Treasury's production checklist. It's off by default; nothing here requires it.
What you built
- A market with a rate you control, backed by a wallet only your desk uses — not the platform-wide relayer.
- Real dual control: nobody, including you with a single API key, can unilaterally fund or reprice this vault again.
- A working swap endpoint with automatic rollback on partial failure.
- Visibility: exposure, a historical-simulation VaR estimate, and a realized PnL rollup, computed from data the engine already has.
Next steps: add Corridor nodes if your desk needs multi-hop liquidity routing (e.g. airtime float → this vault), or wire POST /api/v1/admin/approvals/external so your own service's other critical actions (large refunds, payout batches) go through the same two-person sign-off.