> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xmonaco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Delegated Agents

> Authorize separate signer wallets to trade on behalf of an owner account.

Delegated agents let an owner wallet approve a separate signer for trading automation. The owner keeps custody, withdrawals, collateral movement, and delegation management. The agent can only perform the trading actions allowed by its policy.

Use delegated agents for market-making bots, execution agents, institutional trading systems, and AI agents that should place or manage orders without holding custody.

## Surface Map

| Surface | Use |
| - | - |
| REST | `POST /api/v1/delegated-agents`, `GET /api/v1/delegated-agents`, `GET /api/v1/delegated-agents/owners`, `DELETE /api/v1/delegated-agents/{delegatedAgentId}`, `POST /api/v1/delegated-agents/sessions` |
| TypeScript SDK | `sdk.delegatedAgents.upsertDelegatedAgent()`, `listDelegatedAgents()`, `listDelegatedOwners()`, `revokeDelegatedAgent()`, `createDelegatedSession()`, `sdk.loginAsDelegatedOwner()` |
| React SDK | Use core SDK methods through `useMonacoSDK()` |
| WebSocket | Delegated sessions use the same order, balance, movement, and position channels as owner-scoped sessions |
| gRPC | `DelegatedAgentsService` |

## Model

* The owner owns balances, positions, PnL, perps accounts, and withdrawals.
* The agent signs and submits allowed trading actions.
* Orders are recorded under the owner user ID and carry the owner's wallet, so self-trade prevention and TraderCodes referral rewards treat an agent's order as the owner's own. A single order's create response returns the `delegationId`; batch create results do not.
* Policies can restrict actions, markets, margin accounts, order types, time-in-force, leverage, notional size, expiry, and the IP addresses the agent may create its session from. Concurrent open-order caps are unsupported.
* An owner holds at most 10 active, unexpired agents. Registering another past that is rejected until one is revoked or expires.
* Margin accounts can be resolved by Monaco from owner, market, and optional `strategyKey`.

Delegated agents do not receive the owner private key and do not trade from a separate delegated account. They create an owner-scoped delegated session, then Monaco records each request as owner account activity, while the session keeps the agent address for policy enforcement.

<Note>
  Delegated sessions cannot withdraw or move owner collateral outside the actions allowed by the policy. Every order mutation re-checks the active delegation before it can affect the owner's account.
</Note>

## Delegated Market-Maker Flow

<Steps>
  <Step title="Owner creates the delegation">
    The owner signs normally and creates or updates an agent policy with the external wallet allowed to act, the allowed markets, action types, order types, time-in-force values, leverage limits, notional caps, optional margin account scope, optional IP allowlist, and expiry (14 days from now by default, at most 180 days ahead).

    Only the owner or master account should be able to grant this permission. The market maker never receives the owner private key.
  </Step>

  <Step title="Agent authenticates as itself">
    The market maker logs in with its own wallet and Monaco session. It is not given a separate delegated account and it does not impersonate the owner at wallet-auth time.
  </Step>

  <Step title="Agent discovers delegated owners">
    The agent calls `listDelegatedOwners` to find active owners that delegated to the authenticated agent wallet. The lookup is keyed by the agent wallet address and returns the `ownerUserId` needed to open a delegated session.
  </Step>

  <Step title="Agent adopts the owner-scoped session">
    The agent calls `loginAsDelegatedOwner(ownerUserId)`. The SDK generates a fresh Ed25519 session keypair locally and sends only the session public key to Monaco with the selected `ownerUserId`.

    If the owner has an active, non-expired delegation to that agent wallet, and the agent calls from an address inside the delegation's IP allowlist when it has one, Monaco creates a delegated auth session that acts on the owner user ID while preserving the agent address as the actor, and the SDK installs it as the active session so subsequent calls act on the owner.
  </Step>

  <Step title="Agent signs trading requests">
    With the owner session adopted, the agent signs create, cancel, replace, and close-position requests with the delegated session key. Downstream services see owner-scoped account state plus delegated-agent context.
  </Step>

  <Step title="Monaco re-checks policy on every mutation">
    Before create, cancel, replace, or close-position actions, Monaco reloads the active delegation for `(ownerUserId, agentAddress)`. The request is rejected if the delegation is revoked, inactive, expired, missing, or outside policy limits.
  </Step>

  <Step title="Orders affect the owner account">
    If policy checks pass, the order mutates the owner's allowed spot or perps account. Collateral, positions, PnL, risk, liquidation state, fees, and fills all belong to the owner. The order carries the owner's wallet, not the agent's; to attribute orders to an agent, keep the `delegationId` of the delegation you adopted (from its `listDelegatedOwners` row or the `createDelegatedSession` response) with the order IDs it places, since batch create results do not carry it.
  </Step>

  <Step title="Expiry has two layers">
    A delegated session expires no later than its backing delegation. Every signed request, including reads and session refresh, requires an active, unexpired delegation. Shortening or revoking the grant also ends access for existing sessions. Connected WebSockets enforce this at their next periodic session revalidation (currently every 60 seconds). Existing orders are not automatically canceled.
  </Step>
</Steps>

## Owner Setup

The owner creates or updates an agent policy.

```typescript theme={null}
const agent = await sdk.delegatedAgents.upsertDelegatedAgent({
  agentAddress: "0xagent...",
  name: "btc-mm-bot",
  allowedActions: ["CREATE_ORDER", "CANCEL_ORDER", "REPLACE_ORDER"],
  allowedTradingPairIds: [btcPerpTradingPairId],
  allowedOrderTypes: ["LIMIT"],
  allowedTimeInForce: ["GTC", "IOC"],
  maxLeverage: "10",
  maxOrderNotional: "1000000",
  expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
  ipAllowlist: ["203.0.113.0/24"],
});
```

`agentAddress`, `allowedActions`, and `allowedTradingPairIds` are required in the REST JSON body. Omitting either required array returns `400 Invalid JSON payload`. The four optional arrays — `allowedMarginAccountIds`, `allowedOrderTypes`, `allowedTimeInForce`, and `ipAllowlist` — accept omission, `null`, or `[]` over REST. The TypeScript SDK accepts omission or `[]` for them.

Omit `expiresAt` for the default of 14 days from now; a value more than 180 days ahead returns `400`. Upsert replaces the whole policy, so an update without `expiresAt` also resets the expiry to 14 days from the update.

<Warning>
  Omit `maxOpenOrders`. The server rejects any configured value because this limit was never enforced. Do not display it as an active protection in your UI.
</Warning>

Store `agent.id` for later revocation.

```typescript theme={null}
await sdk.delegatedAgents.revokeDelegatedAgent(agent.id);
```

## Agent Session

The agent authenticates with its own wallet, discovers owners that delegate to it, then adopts an owner-scoped delegated session with `loginAsDelegatedOwner`. Adopting the session installs a fresh owner-scoped session key as the SDK's active auth state, so every subsequent read and order call acts on the owner account.

```typescript theme={null}
await sdk.login(agentClientId);

const { owners } = await sdk.delegatedAgents.listDelegatedOwners();
const ownerUserId = owners[0].ownerUserId;

const auth = await sdk.loginAsDelegatedOwner(ownerUserId);

console.log(auth.user.id); // the owner — the identity subsequent calls act on
console.log(auth.user.address); // the agent wallet — the session's actor, used for policy enforcement
console.log(auth.expiresAt); // session expiry
```

`listDelegatedOwners` is keyed on the agent's authenticated wallet and returns only non-revoked, non-expired delegations. Use `loginAsDelegatedOwner` rather than calling `createDelegatedSession` directly: the latter registers a session but does not switch the SDK to it, so calls would keep signing with the agent's own session and act on the agent account instead of the owner.

Keep the agent's ordinary login in a separate SDK instance or secure credential store: adopting a delegated session replaces the current instance's auth state. `refreshAuth()` can extend a still-valid session only up to the grant's expiry; it cannot renew the owner's authorization or recover an expired session. After grant expiry, the owner must renew the grant. An expired session requires a fresh delegated session using the agent's own wallet login.

Grant expiry blocks access while the grant is expired; it does not permanently revoke session keys. After owner reauthorization, an existing unexpired session may become usable again, including a session issued before the grant was shortened. Successfully revoking the agent also marks its existing sessions revoked; renewing the grant does not revive those revoked sessions. Reconcile a failed revocation before reauthorizing the agent.

The delegated session uses owner account state with delegated-agent context:

| Field | Meaning |
| - | - |
| Authenticated user | Owner user ID |
| Actor address | Agent wallet address |
| Account mode | Delegated |
| Withdrawal permission | Disabled |
| Risk, collateral, positions, PnL | Owner account |
| Delegation ID | Returned by `listDelegatedOwners`, by `createDelegatedSession` and on a single order's create response (not on batch create results); not stored on the order |

## Trading With Auto-Resolved Perps Accounts

For normal delegated perp trading, omit `marginAccountId`. Monaco resolves or creates the owner-owned bucket for the market and optional strategy.

```typescript theme={null}
await sdk.trading.placeLimitOrder(btcPerpId, "BUY", "1", "65000", {
  tradingMode: "MARGIN",
  leverage: "5",
  strategyKey: "btc-mm-a",
});
```

Use an explicit `marginAccountId` only for advanced reconciliation or fixed-bucket institutional flows.

### Price-Bounded Immediate Execution

For a delegated grant with `maxOrderNotional`, use a `LIMIT` order with an explicit `price` and `timeInForce: "IOC"` when partial fills are acceptable. The grant must allow `CREATE_ORDER`, the market or margin account, `LIMIT`, and `IOC`. IOC fills immediately at the limit or better and cancels any unfilled remainder; it never leaves that remainder resting.

Choose the limit from a fresh reference price and your execution tolerance, using decimal arithmetic:

* `BUY`: maximum price = reference × (1 + toleranceBps / 10000), rounded **down** to the market's `tickSize`.
* `SELL`: minimum price = reference × (1 - toleranceBps / 10000), rounded **up** to `tickSize`.
* Round quantity down to the market's `quantityStepSize` within base-asset precision, then check minimum/maximum order size and `limitPrice × quantity <= maxOrderNotional`. Submit price and quantity as human-readable decimal strings, not raw token units. The cap is inclusive and uses the entire requested quantity, even if only part can fill.

For example, a BUY reference of `60000` with a 100 bps tolerance gives a `60600` limit. If the market permits `0.0001` base units and that price tick, its requested notional is `6.06` quote units:

```typescript theme={null}
const order = await sdk.trading.placeLimitOrder(
  btcPerpId,
  "BUY",
  "0.0001",
  "60600",
  {
    tradingMode: "MARGIN",
    marginMode: "CROSS",
    leverage: "1",
    timeInForce: "IOC",
  },
);
```

Do not set `postOnly: true` with IOC or pass `slippageToleranceBps` on this limit order. The explicit price is its execution bound. A capped `MARKET` request without a price is rejected with `Delegated agent maxOrderNotional requires a limit price`; a market-order slippage tolerance does not supply that missing policy input.

Inspect `matchResult.status`, `totalFilled`, and `remainingQuantity`. A partial IOC fill is terminal `CANCELLED` with a nonzero `totalFilled`; an IOC with no executable liquidity is `REJECTED`. A successful submission does not promise a full fill. Reconcile fills before submitting another order for any remaining intent.

`maxOrderNotional` checks submitted price × quantity at admission. It is not an aggregate exposure, collateral, or fee budget. A SELL can execute above its minimum price, so its realized quote proceeds can exceed that product. Fees and margin-risk checks still apply, and a stale reference can result in a partial fill or no fill.

For margin orders, [simulate the risk bucket order](/developers/trade#risk-buckets-and-pre-trade-simulation) against its current exposure before placement. A preview does not reserve collateral: balances, fees, exposure, and prices can change before the order arrives. Keep the reported margin amounts as decimals and do not round away a positive shortfall, even a very small one; admission still requires actual funding.

### Close Position

Closing a perp position is treated like order creation because Monaco submits a close or reduce order. Delegated sessions must pass the same policy checks used for create-order actions before a close can be submitted.

For batch close-all flows, delegated sessions should be scoped to an explicit `tradingPairId`. That prevents a broadly authenticated delegated session from closing every open position across all markets without a market-specific policy check.

## Policy Design

Start narrow and widen only when the agent needs more capability:

| Policy field | Use |
| - | - |
| `allowedActions` | Required; `CREATE_ORDER`, `CANCEL_ORDER`, `REPLACE_ORDER`, `READ`, `MANAGE_RISK_BUCKETS`; any other value is rejected. `[]` or `["READ"]` is a read-only key |
| `allowedTradingPairIds` | Required; trading pair UUIDs the agent may trade; use `[]` to leave markets unrestricted for an account-bound agent |
| `allowedMarginAccountIds` | Optional; margin accounts the agent is bound to; omitted or empty leaves the account unrestricted |
| `allowedOrderTypes` | Optional; restrict to `LIMIT`, `MARKET`, or both; omitted or empty is unrestricted |
| `allowedTimeInForce` | Optional; restrict to `GTC`, `IOC`, or `FOK`; omitted or empty is unrestricted |
| `maxLeverage` | Cap leverage requested by the agent |
| `maxOrderNotional` | Inclusive cap on submitted price × quantity in quote units; requires an explicit price |
| `maxOpenOrders` | Unsupported; setting it is rejected |
| `expiresAt` | Force periodic re-authorization; defaults to 14 days from now, at most 180 days ahead |
| `ipAllowlist` | Optional; up to 20 IP addresses or CIDR ranges the agent may create its delegated session from; omitted or empty allows any address |

Each listed scope must match: an agent listing a margin account trades only that account, and only on its listed markets if it lists any. A market-only agent (no `allowedMarginAccountIds`) trades its listed markets in every margin account. Both scopes empty deny all orders; empty scopes do not grant access to every market or account. Active delegated sessions can read owner account data; trading scopes do not filter that data.

The IP allowlist is checked when the agent creates its delegated session, against the address the request arrives from. A caller outside every listed range gets `403`. Only session creation is checked: a session already created keeps working from any address until it expires or the grant is revoked, so revoke the grant to cut off a session after narrowing the allowlist. Entries are stored normalized to CIDR form, so a bare address reads back as `/32` (IPv4) or `/128` (IPv6).

## Permission Summary

| Capability | Delegated behavior |
| - | - |
| Read owner data | Allowed for every active delegated session; a policy of only `READ`, or no action, cannot manage orders |
| Create orders | Allowed only when `CREATE_ORDER` and all market/account/order limits pass |
| Cancel orders | Allowed only when `CANCEL_ORDER` and market/account scope pass |
| Replace orders | Allowed only when `REPLACE_ORDER` and replacement limits pass |
| Close positions | Treated as order creation and checked against delegated policy |
| Fund risk buckets | Allowed only when `MANAGE_RISK_BUCKETS` is listed, for a margin account in `allowedMarginAccountIds`: the account's free collateral moves into one of its own risk buckets (`POST /api/v1/margin/risk-buckets/collateral/transfer-in`). If the policy lists markets, only an isolated risk bucket on a listed market |
| Other collateral moves | Not allowed: no transfer between margin accounts, out of a risk bucket, or to or from the wallet |
| Withdrawals | Not allowed through delegated sessions |
| Collateral and PnL | Belong to the owner, not the agent |
| Expiry or revocation | Ends authentication, reads, trading, and refresh; connected WebSockets enforce it on periodic revalidation |

## UI Rules

* Show the owner which wallet is authorized and what it can do.
* Make revocation obvious and immediate.
* Display policy expiry and market restrictions near agent status.
* To show which agent placed an order, store the adopted delegation's `delegationId` with the order IDs it places; order history does not carry the agent.
* For bots and AI agents, prefer narrow market and notional limits by default.

## Related References

* [Order Management](/developers/trade) for order actions agents can perform
* [Order Management](/developers/trade#risk-buckets-and-pre-trade-simulation) for strategy-key bucket routing
* [TypeScript delegated agents](/sdk/typescript/delegated-agents) for SDK method reference
* [MCP Server](/sdk/mcp-server) for AI-agent integrations


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.