Medici Protocol

Strategy Engine

Declarative strategy intents — define what you want, the engine runs it. Covers the intent model, template presets, validation, parameter derivation, and agent orchestration.

1. Intent model

A StrategyIntent is a declarative specification of an index-tracking strategy. You describe what you want — the ticker to track, the capital to deploy, the risk constraints — and the Strategy Engine activates agents that execute it. You never write agent orchestration logic; the engine derives it from your parameters.

Lifecycle states

StateMeaningAllowed transitions
active Agents are running. Vaults are allocated, rolls execute at threshold, drift is monitored. paused, completed, cancelled
paused Execution is suspended. Vaults remain in place but no rolls or rebalances occur. Useful for maintenance windows or manual review. active, cancelled
completed Strategy reached its natural end — all positions settled, capital returned. Terminal state. none
cancelled User-initiated cancel. Positions are settled and vaults closed. Terminal state. none
error Unrecoverable fault — agent crash, on-chain revert, or invariant violation. Requires manual intervention. Terminal state. none
Ownership: Every intent has an owner field set to the Canton party (derived from the JWT sub claim) that created it. Only the owner can pause, resume, update, or cancel an intent. Non-owner requests receive HTTP 403.

What the engine does

When an intent transitions to active, the Strategy Engine:

  1. Validates all parameters against known-good ranges and cross-field sanity rules.
  2. Derives operational parameters (expected rolls/year, estimated annual cost).
  3. Selects the required agent set based on strategy type.
  4. Allocates vaults on-chain via the PoolVault template.
  5. Starts the rebalancing loop: monitor price, roll at threshold, enforce constraints.
  6. Writes runtime state back to the intent (runtime field) so the frontend can poll it.

2. Full schema

The canonical TypeScript type for StrategyIntent (from packages/agent-sdk/src/intents.ts). Every field is shown with its type and purpose.

// ── Type aliases ──
type StrategyType = 'treasury' | 'hedge' | 'leverage' | 'yield' | 'custom';
type IntentStatus = 'active' | 'paused' | 'completed' | 'cancelled' | 'error';
type RollStrategy = 'standard' | 'shortDated' | 'manual';
type NotificationChannel = 'email' | 'push' | 'inapp' | 'webhook';

// ── Sub-objects ──
interface CollateralSpec {
  asset: 'USDC' | 'WETH' | 'WBTC';  // collateral asset
  amount: string;                    // decimal string, e.g. "10000.00"
}

interface IntentConfig {
  strikeRatio: number;          // 0.01–1.0, e.g. 0.50 = 50% strike
  maturityDays: number;         // 1–365, option maturity in calendar days
  rebalanceThreshold: number;   // >= 1.0, rotate when price < strike * threshold
  autoRoll: boolean;            // auto-roll at maturity / threshold breach
  rollStrategy: RollStrategy;   // standard | shortDated | manual
}

interface IntentConstraints {
  maxAnnualDriftBps: number;      // max tolerated annual drift (basis points)
  maxSlippagePerRollBps: number;  // max slippage per roll (basis points)
  maxCapital: string;             // hard capital cap (decimal string)
  stopLossDriftBps: number;       // unwind if drift exceeds this (basis points)
}

interface IntentNotifications {
  onRoll: boolean;               // notify on each roll
  onThresholdBreach: boolean;    // notify when rebalance threshold is crossed
  onStopLoss: boolean;           // notify if stop-loss triggers
  channels: NotificationChannel[];  // at least one channel
}

interface IntentRuntime {
  vaultIds: string[];          // active on-chain vault contract IDs
  currentDriftBps: number;     // live annualized drift in basis points
  totalRolls: number;          // count of rolls executed so far
  totalCostBps: number;        // cumulative realized slippage in bps
  lastRollAt: string;          // ISO 8601 timestamp of last roll
  lastRollCostBps: number;     // slippage cost of last roll in bps
}

// ── Top-level intent ──
interface StrategyIntent {
  id: string;                      // auto-generated: intent-{unixms}-{8hex}
  owner: string;                   // Canton party (from JWT sub)
  type: StrategyType;              // strategy preset
  status: IntentStatus;            // lifecycle state
  statusReason?: string;           // e.g. "Stop-loss triggered: drift exceeded 5%"

  collateral: CollateralSpec;      // what capital to deploy
  ticker: string;                  // e.g. "BTC/USDC"

  config: IntentConfig;            // strategy parameters
  constraints: IntentConstraints;  // risk guardrails
  notifications: IntentNotifications;  // alert preferences

  runtime?: IntentRuntime;         // live state (populated by agents)
}
Create vs full intent: When creating an intent, you send a CreateIntentRequest — the StrategyIntent shape minus id, status, and runtime. Those three are populated by the engine. Updates use UpdateIntentRequest — a partial of only the mutable runtime fields.

3. Strategy templates

Five pre-configured templates with backtest-optimal defaults. Each template provides a complete config + constraints + notifications triple. Use createFromTemplate(type, overrides) to merge user values over the template defaults.

Type Label strikeRatio maturityDays rebalanceThreshold autoRoll rollStrategy maxAnnualDriftBps maxSlippagePerRollBps maxCapital stopLossDriftBps
treasury Treasury Management 0.50 30 1.5 true standard 200 10 1,000,000,000 500
hedge Portfolio Hedge 0.30 14 1.3 true shortDated 500 15 50,000,000 800
leverage Leveraged Exposure 0.80 7 1.2 true shortDated 2000 25 1,000,000 5000
yield Yield Generation 0.50 60 2.0 true standard 300 10 10,000,000 600
custom Custom Strategy 0.50 30 1.5 false manual 1000 20 1,000,000 1000

Notification defaults

TypeonRollonThresholdBreachonStopLosschannels
treasurytruetruetrueinapp, webhook
hedgetruetruetrueinapp, push, webhook
leveragetruetruetrueinapp, push
yieldtruefalsetrueinapp
customfalsefalsefalseinapp

Template descriptions

treasury — Treasury Management
Conservative yield generation for DAO treasuries. Low drift (200 bps cap), capital preservation focus. Suitable for stablecoin holdings seeking modest yield without meaningful directional risk. Default max capital of 1B reflects DAO scale.
hedge — Portfolio Hedge
Protect existing crypto holdings against drawdowns. Lower strike (0.30 = cheaper protection, lower coverage). Short-dated rolls (14-day maturity, shortDated strategy). For active portfolio managers hedging directional exposure.
leverage — Leveraged Exposure
Amplified directional exposure. Highest strike (0.80) for maximum delta per unit of capital. Most aggressive ranges — 2000 bps drift tolerance, 5000 bps stop-loss. 7-day maturity with shortDated rolls. For sophisticated traders comfortable with significant downside risk.
yield — Yield Generation
Passive yield from options premium. Mid-range strike (0.50), longest maturity (60 days) for lowest roll frequency and cost. Threshold breach notifications off by default — designed for depositors seeking steady returns without active management.
custom — Custom Strategy
Full manual control — no defaults enforced, all parameters must be explicitly configured. autoRoll and all notifications off by default. rollStrategy is manual. For advanced users with specific requirements not covered by the presets.

4. Validation rules

Every intent goes through a two-stage validation pipeline before it can be activated. Stage 1 blocks invalid intents with errors; Stage 2 produces warnings for configurations that are technically legal but likely misconfigured.

4.1 Parameter ranges (hard errors)

ParameterMinMaxNotes
config.strikeRatio0.011.0Exclusive min — must be > 0
config.maturityDays1365Integer
config.rebalanceThreshold1.0100.0Values below 1.0 are nonsensical (price can't go below zero)
constraints.maxAnnualDriftBps010,00010,000 bps = 100% annual drift
constraints.maxSlippagePerRollBps0100100 bps = 1% per roll
constraints.stopLossDriftBps010,00010,000 bps = unwind at 100% drift
Required fields: type, collateral (with asset and amount), ticker, config, constraints, and notifications are all required. The ticker must match ^[A-Z]{2,5}/[A-Z]{2,5}$. notifications.channels must have at least one entry from the allowed set: email, push, inapp, webhook. collateral.asset must be one of USDC, WETH, WBTC.

4.2 Cross-field sanity checks (warnings)

CheckConditionSeverityRationale
Stop-loss vs drift tolerance stopLossDriftBps <= maxAnnualDriftBps warning Stop-loss may trigger during normal drift — your strategy will unwind prematurely. Increase stopLossDriftBps or tighten maxAnnualDriftBps.
Short maturity + standard rolls maturityDays < 7 AND rollStrategy == 'standard' warning Very short maturity with standard roll strategy generates excessive roll costs. Consider shortDated roll strategy for sub-7-day maturities.
High strike on non-leverage type strikeRatio > 0.80 AND type != 'leverage' warning Strike above 0.80 is aggressive — exceptionally high delta per unit of capital with proportionally higher drift. Consider the leverage type which is designed for this range.

4.3 Type-specific warnings

TypeConditionWarning
treasury maxAnnualDriftBps > 500 Treasury strategy with high drift tolerance — the treasury preset caps at 200 bps. Values above 500 bps are outside the conservative envelope.
yield maturityDays < 14 Yield strategy benefits from longer maturity (>=30 days) to reduce roll frequency and cost. Short maturities erode yield through slippage.

4.4 Validation response shape

Validation returns a structured result — never throws. Errors block activation; warnings are advisory.

interface ValidationResult {
  valid: boolean;     // false if any errors exist
  errors: string[];   // hard blockers — intent cannot activate
  warnings: string[]; // advisory — intent can activate but may be misconfigured
}

5. Parameter derivation

When the Strategy Engine activates an intent, it derives three operational parameters from the config and constraints. These drive the agent orchestration plan and appear in the frontend as pre-flight estimates.

Derived parameterFormulaExample (BTC/USDC, treasury defaults)
expectedRollsPerYear Math.ceil(365 / maturityDays) ceil(365 / 30) = 13 rolls/year
estimatedMaxAnnualCostBps expectedRollsPerYear * maxSlippagePerRollBps 13 * 10 = 130 bps/year (1.30%)
isPaperOptimal strikeRatio === 0.50 && maturityDays === 30 true — treasury uses paper-optimal defaults
Paper-optimal detection: The reference paper (ethresear.ch) identifies a 50% strike with 30-day maturity as the sweet spot — maximal tracking fidelity with minimal drift. When isPaperOptimal is true, your config matches this baseline exactly. The treasury and yield presets are paper-optimal; hedge and leverage deliberately deviate for their respective use cases.

Cost model quick reference

TemplatematurityDaysmaxSlippagePerRollBpsrolls/yearmax annual cost (bps)max annual cost (%)
treasury3010131301.30%
hedge1415274054.05%
leverage72553132513.25%
yield60107700.70%
custom3020132602.60%

These are worst-case estimates — actual costs depend on market conditions at roll time. The paper documents empirical drift of ~1–4%/year; real-world costs are typically 30–50% of these ceilings.

6. Agent orchestration

Each strategy type requires a specific set of agents to execute. The Strategy Engine selects and launches them automatically when the intent transitions to active.

AgentRoleRequired by
Oracle Agent Publishes consensus prices on-chain (PublishedPrice). Required for settlement and drift calculation. Without it, no strategy can function. All types
Pool Agent Manages PoolVault lifecycle: creates vaults, processes deposits/withdrawals, maintains the option inventory. One per collateral asset. All types
Rebalance Agent Monitors drift, executes rolls when rebalanceThreshold is crossed, enforces maxSlippagePerRollBps. The core execution loop. All types
Market Maker Agent Provides liquidity by quoting bid/ask spreads on option positions. Reduces roll slippage. Optional but recommended for leverage and hedge (frequent rolls). hedge, leverage (recommended); others (optional)
Settlement Agent Settles matured options, distributes proceeds. Runs on a timer aligned to maturity windows. All types
Position Monitor Checks for stuck positions, invariant violations, and on-chain state drift. Emits error status transitions when invariants break. All types (always on)
Swap Taker Agent Executes asset swaps (e.g. USDC to WBTC) for collateral rotation. Only needed when the collateral asset differs from the pool's denomination. Conditional (cross-asset only)
Agent composition per strategy:
StrategyCore agentsRecommended extrasNotes
treasury Oracle, Pool, Rebalance, Settlement, Position Monitor Market Maker (optional) Steady-state operation. 13 rolls/year — Market Maker adds marginal improvement.
hedge Oracle, Pool, Rebalance, Settlement, Position Monitor Market Maker (strongly recommended) 27 rolls/year — Market Maker significantly reduces cumulative slippage at this frequency.
leverage Oracle, Pool, Rebalance, Settlement, Position Monitor Market Maker (strongly recommended), Swap Taker 53 rolls/year — highest roll frequency. Market Maker is nearly mandatory to keep costs manageable. Swap Taker if collateral != pool denomination.
yield Oracle, Pool, Rebalance, Settlement, Position Monitor Lowest maintenance. 7 rolls/year — Market Maker overhead not justified.
custom Oracle, Pool, Rebalance, Settlement, Position Monitor Depends on config Agent set follows the derived parameters, not the template. The engine selects based on your config values.

7. TypeScript example

End-to-end example: create a BTC/USDC treasury intent, validate it, activate it, and poll for runtime state. Production-quality code using the Agent SDK.

import {
  createFromTemplate,
  validateStrategyConfig,
  deriveParams,
  type StrategyIntent,
} from '@medici/agent-sdk';

// ── Step 1: Create from template with overrides ──
const strategy = createFromTemplate('treasury', {
  config: { strikeRatio: 0.55 },  // slightly more aggressive than default 0.50
});

console.log('Config:', strategy.config);
console.log('Constraints:', strategy.constraints);
console.log('Notifications:', strategy.notifications);

// ── Step 2: Validate ──
const validation = validateStrategyConfig(
  'treasury',
  strategy.config,
  strategy.constraints,
);

if (!validation.valid) {
  console.error('Validation errors:', validation.errors);
  process.exit(1);
}
if (validation.warnings.length > 0) {
  console.warn('Warnings:', validation.warnings);
}

// ── Step 3: Derive operational params ──
const params = deriveParams(strategy.config, strategy.constraints);
console.log('Expected rolls/year:', params.expectedRollsPerYear);
console.log('Max annual cost (bps):', params.estimatedMaxAnnualCostBps);
console.log('Paper-optimal:', params.isPaperOptimal);

// ── Step 4: Build the create-intent request ──
const request = {
  type: 'treasury' as const,
  ticker: 'BTC/USDC',
  collateral: { asset: 'USDC' as const, amount: '50000.00' },
  config: strategy.config,
  constraints: strategy.constraints,
  notifications: strategy.notifications,
};

// ── Step 5: Submit to Ledger Service ──
const response = await fetch('https://canton.int.medici.loan/api/v1/intents', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
  },
  body: JSON.stringify(request),
});

if (!response.ok) {
  const err = await response.json();
  throw new Error(`Create failed: ${err.error} — ${err.message}`);
}

const { intent } = await response.json();
console.log('Intent created:', intent.id, 'status:', intent.status);

// ── Step 6: Poll runtime state (every 30s) ──
const poll = async (intentId: string) => {
  const res = await fetch(
    `https://canton.int.medici.loan/api/v1/intents/${intentId}`,
    { headers: { Authorization: `Bearer ${token}` } },
  );
  const { intent: fresh } = await res.json();
  if (fresh.runtime) {
    console.log(`Drift: ${fresh.runtime.currentDriftBps} bps | Rolls: ${fresh.runtime.totalRolls} | Cost: ${fresh.runtime.totalCostBps} bps`);
  }
};

const interval = setInterval(() => poll(intent.id), 30_000);

// Stop polling when done
process.on('SIGINT', () => { clearInterval(interval); });

8. cURL example

Create, inspect, and manage a strategy intent directly via the Ledger Service REST API. All examples use BTC/USDC as the tracked ticker.

8.1 Create a treasury intent

# Set your base URL and JWT token
BASE="https://canton.int.medici.loan"
TOKEN="$(curl -s -X POST https://keycloak.int.medici.loan/realms/medici/protocol/openid-connect/token \
  -d grant_type=password \
  -d client_id=app-user-validator \
  -d username=alice \
  -d password="$PASSWORD" \
  -d client_secret="$CLIENT_SECRET" | jq -r '.access_token')"

# Create the intent
curl -s -X POST "$BASE/api/v1/intents" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "treasury",
    "ticker": "BTC/USDC",
    "collateral": {"asset": "USDC", "amount": "50000.00"},
    "config": {
      "strikeRatio": 0.50,
      "maturityDays": 30,
      "rebalanceThreshold": 1.5,
      "autoRoll": true,
      "rollStrategy": "standard"
    },
    "constraints": {
      "maxAnnualDriftBps": 200,
      "maxSlippagePerRollBps": 10,
      "maxCapital": "50000.00",
      "stopLossDriftBps": 500
    },
    "notifications": {
      "onRoll": true,
      "onThresholdBreach": true,
      "onStopLoss": true,
      "channels": ["inapp", "webhook"]
    }
  }' | jq .

# Response (201):
# {
#   "intent": {
#     "id": "intent-1719763200000-a1b2c3d4",
#     "owner": "alice",
#     "type": "treasury",
#     "status": "active",
#     "ticker": "BTC/USDC",
#     "collateral": {"asset": "USDC", "amount": "50000.00"},
#     "config": { ... },
#     "constraints": { ... },
#     "notifications": { ... },
#     "createdAt": "2026-06-30T12:00:00Z",
#     "updatedAt": "2026-06-30T12:00:00Z"
#   },
#   "fetchedAt": "2026-06-30T12:00:00Z"
# }

8.2 List, pause, and cancel

# List all active intents
curl -s "$BASE/api/v1/intents?status=active" \
  -H "Authorization: Bearer $TOKEN" | jq .

# Get a specific intent
INTENT_ID="intent-1719763200000-a1b2c3d4"
curl -s "$BASE/api/v1/intents/$INTENT_ID" \
  -H "Authorization: Bearer $TOKEN" | jq .

# Pause it
curl -s -X POST "$BASE/api/v1/intents/$INTENT_ID/pause" \
  -H "Authorization: Bearer $TOKEN" | jq .

# Resume it
curl -s -X POST "$BASE/api/v1/intents/$INTENT_ID/resume" \
  -H "Authorization: Bearer $TOKEN" | jq .

# Cancel it (soft-delete — terminal)
curl -s -X DELETE "$BASE/api/v1/intents/$INTENT_ID" \
  -H "Authorization: Bearer $TOKEN" | jq .

# List strategy templates (no auth required)
curl -s "$BASE/api/v1/strategies" | jq .

# Portfolio summary
curl -s "$BASE/api/v1/portfolio" \
  -H "Authorization: Bearer $TOKEN" | jq .

8.3 Update an intent

# Update config and constraints (partial — only changed fields)
curl -s -X PUT "$BASE/api/v1/intents/$INTENT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "config": {
      "strikeRatio": 0.55,
      "maturityDays": 45
    },
    "constraints": {
      "maxAnnualDriftBps": 300
    }
  }' | jq .
Cannot update terminal intents: Intents with status completed or cancelled are immutable — PUT returns HTTP 500 with message "intent is completed/cancelled and cannot be modified".

Full API Reference →