Medici Protocol

Agent Delegation

How Medici's autonomous agents act on a user's behalf: two authorization planes, identity-only tokens, and delegation as an on-ledger contract whose bounds Canton itself enforces.

Three questions per request

Every request into the Ledger Service is classified by three orthogonal facts:

QuestionAnswered byNotes
Who is calling? Verified JWT → fleet-roster match on client_id Yields is this a fleet agent — a boolean about the caller, deliberately not a kind of authority
Whose authority applies? The authorization plane bearer — the caller's own Canton user rights authorize the action; signature — the user's signature over the prepared transaction authorizes it
What is permitted? Canton rights + the delegation's scope Rights are reconciler-owned (see Identity Reconciler); delegation bounds are re-checked on-ledger at execution time

Tokens carry identity, never authority. Canton resolves rights from the token's sub via the Canton user record and ignores any actAs/readAs claims a token carries. Every handler that touches Canton bridges the caller's verified token (WithCallerToken) so it acts with the caller's rights — skipping the bridge doesn't 401, it silently acts as the service. Agents additionally hard-fail at boot via POST /api/v1/auth/verify if any expected right is missing.

Mechanism A — proposal at intent creation

The default, in production. When a user creates a strategy intent, they sign the resulting SplitRequest proposal in their own authenticated session; the StrategyAgent later exercises OperatorExecute on that user-signed proposal. The agent holds no standing rights over any user's party — its authority is one proposal, created by the principal, consumable once. See Intent Execution Authority.

Mechanism B — StrategyDelegation (on-ledger power of attorney)

For recurring lifecycle actions (rolls, rebalances, stop-losses) whose parameters can't be pre-signed, the user signs a single StrategyDelegation contract — signatory owner, observer operator — granting scoped, time-bounded, revocable authority:

BoundFieldEnforcement
MarketsallowedTickersDAML assertion in the choice body
Size per actionmaxNotionalDAML assertion
Action typesallowedActionsDAML assertion
LifetimeexpiryChecked against ledger time (getTime) — not caller-forgeable
Total usesactionCount / maxActionsSound because every use consumes and recreates the contract
Re-delegationmaxDepth0 by default — no sub-delegation

Every delegated action consumes the contract and recreates it with the counter advanced — stale contract IDs can't be replayed and the counter can't be raced. Only the owner can Revoke (immediate — the contract is archived) or UpdateScope; the operator deliberately holds no revocation right.

API

POST   /api/v1/delegations      # create — owner is resolved from YOUR verified
                                # token via Canton user lookup, never from the body
GET    /api/v1/delegations      # list delegations visible to the caller
DELETE /api/v1/delegations/:id  # revoke (owner only)

Requires an onboarded Canton user (POST /api/v1/onboard first). Body fields mirror the scope table above: delegationId, operator, allowedTickers, maxNotional, allowedActions, expiry (ISO 8601), optional maxActions, maxDepth.

The signature plane — delegation without custody

External parties (CIP-103) keep their own keys: the service prepares transactions and returns hashes; the user's key signs; the ledger accepts the signature as the authority. Middleware enforces the plane invariant before every signature-plane submission: the service must hold actAs on no external party — one grant is a violation and the request is refused. The identity manifest is lint-checked so such a grant can't even be declared. See the CIP-103 flow example.

Oversight

Further reading