# Subaccounts

Create, share, audit, and stream subaccounts, and manage the reusable policy templates that govern them.

`client.subaccounts` creates, manages, shares, and audits the subaccounts visible to the authenticated caller, and streams their updates. The nested `client.subaccounts.policies` sub-service manages the reusable permission templates you attach to subaccounts. Every method is authenticated except the public `listRoles` catalog read.

Subaccounts isolate balances and permissions under your root account. Delegated members get a role, and a policy caps what any member can do. For the task-oriented walkthrough see the [accounts and balances guide](https://testnet.polyester.com/docs/sdk/typescript/guides/accounts-and-balances).

## Methods

| Method                    | Summary                                                           |
| ------------------------- | ----------------------------------------------------------------- |
| `list`                    | List subaccounts owned by or shared with the caller.              |
| `get`                     | Fetch one subaccount with its keys, policy, members, and invites. |
| `createChallenge`         | Request the next smart account and its authorization message.     |
| `create`                  | Create a subaccount from a signature proof.                       |
| `update`                  | Update a subaccount's label or status.                            |
| `inviteMember`            | Invite another account to a role on a subaccount.                 |
| `respondInvite`           | Accept, decline, or cancel an invitation.                         |
| `listInvites`             | List invitations by direction.                                    |
| `listRoles`               | Read the public built-in role and permission catalog.             |
| `getEffectivePermissions` | Read the caller's role and permissions for a subaccount.          |
| `listMembers`             | List a subaccount's owner and delegated members.                  |
| `updateMemberRole`        | Change a member's role in place.                                  |
| `removeMember`            | Revoke a member's delegated access.                               |
| `setMemberMfaRequirement` | Toggle the owner-controlled MFA gate.                             |
| `listEvents`              | Page the subaccount audit trail.                                  |
| `subscribe`               | Stream subaccount updates.                                        |
| `subscribeApiKeys`        | Stream API key updates.                                           |

### `list(options?)`

Returns `{ totalCreated, subaccounts }` for the accounts owned by or shared with the caller. `totalCreated` counts every subaccount ever created, including soft-deleted ones. Use [`createChallenge`](#createchallengeinput-options) for the canonical next smart-account address and salt nonce.

```ts
const { totalCreated, subaccounts } = await client.subaccounts.list();
console.log(`${subaccounts.length} active of ${totalCreated} created`);
```

### `get(input, options?)`

Fetches one subaccount as an aggregated dashboard view: the subaccount plus its `apiKeys`, `policy`, `members`, and outgoing `invites`. Throws if the subaccount is missing.

```ts
const detail = await client.subaccounts.get({ subaccountId });
console.log(detail.label, detail.policy.name, detail.members.length);
```

`SubaccountIdInput` is `{ subaccountId }`.

### `createChallenge(input, options?)`

Requests the canonical next subaccount smart account and a short-lived EIP-191 authorization message bound to the authenticated root account and the selected owner EOA. Sign the exact `message` through the returned smart account, then pass it to `create`. A new challenge replaces the previous one.

```ts
const challenge = await client.subaccounts.createChallenge({
	ownerAddress: rootOwnerAddress,
	uri: window.location.origin,
});
const signer = deriveSubaccountSigner(challenge.smartAccountSaltNonce);
const signature = await signer.signMessage(challenge.message);
```

```ts
interface SubaccountChallenge {
	message: string;
	smartAccountAddress: string;
	smartAccountSaltNonce: number;
	expiresAt?: number; // epoch ms
	polyesterChainId: number;
}
```

An expired, replaced, or replayed authorization fails `create` with `SubaccountChallengeInvalidError`; request a fresh challenge and sign again.

### `create(input, options?)`

The root account must have accepted the current terms. Use [`client.auth.acceptTerms()`](https://testnet.polyester.com/docs/sdk/typescript/reference/auth#accepttermsoptions) from an interactive JWT session after the user agrees. Missing acceptance produces the typed auth detail `AUTH_TERMS_NOT_ACCEPTED`.

Creates a subaccount under the caller's root account using the smart-account address, exact message, and smart-account signature from `createChallenge`, returning a `CreateSubaccountResult`.

```ts
const result = await client.subaccounts.create({
	label: "Trading bot",
	smartAccountAddress: challenge.smartAccountAddress,
	message: challenge.message,
	signature,
});
console.log(result.subaccountId, result.smartAccountSaltNonce);
```

> **Browser apps have a shortcut**
>
> In the browser, prefer [`client.auth.createSubaccount`](https://testnet.polyester.com/docs/sdk/typescript/reference/auth): it requests the challenge and signs it with an account signer for you, so you never assemble the proof by hand.

```ts
interface CreateSubaccountInput {
	smartAccountAddress: string;
	message: string;
	signature: string;
	label?: string;
	icon?: string;
	color?: string;
}

interface CreateSubaccountResult {
	subaccountId: string;
	totalCreated: number;
	smartAccountSaltNonce: number;
	revision: string;
}
```

Sign the challenge message exactly as issued. This proof may use a Safe or ERC-6492-wrapped signature.

### `update(input, options?)`

Updates mutable display and status fields such as `label` and `status`.

```ts
await client.subaccounts.update({
	subaccountId,
	expectedRevision: detail.revision,
	label: "Renamed",
});
```

```ts
interface UpdateSubaccountInput {
	subaccountId: string;
	expectedRevision: string;
	label?: string;
	icon?: string;
	color?: string;
	status?: "active" | "disabled";
}
```

The SDK does not delete subaccounts. Disable one to prevent further use:

```ts
await client.subaccounts.update({
	subaccountId,
	expectedRevision: detail.revision,
	status: "disabled",
});
```

Updates use optimistic concurrency. Refetch after a `RevisionConflictError` and use the latest `revision`; never retry the stale mutation unchanged.

### `inviteMember(input, options?)`

Creates or refreshes a pending invitation granting a role on a subaccount to another root account, returning the `SubaccountInvite`.

```ts
const invite = await client.subaccounts.inviteMember({
	subaccountId,
	granteeAccountId,
	role: "trader",
});
```

```ts
interface InviteSubaccountMemberInput {
	subaccountId: string;
	granteeAccountId: string;
	role: "owner" | "admin" | "treasury" | "leveraged_trader" | "trader" | "viewer";
}
```

### `respondInvite(input, options?)`

Accepts or declines an incoming invite, or cancels an outgoing one, returning the updated invite.

```ts
await client.subaccounts.respondInvite({ inviteId, action: "accept" });
```

```ts
interface RespondSubaccountInviteInput {
	inviteId: string;
	action: "accept" | "decline" | "cancel";
}
```

### `listInvites(input, options?)`

Returns invitations for the caller, newest first, filtered by direction.

```ts
const incoming = await client.subaccounts.listInvites({ direction: "incoming" });
```

```ts
interface ListSubaccountInvitesInput {
	direction?: "incoming" | "outgoing" | ""; // "" lists all
}
```

### `listRoles(options?)`

Returns the public built-in role and permission catalog. Each role has a `role`, `displayName`, `description`, `assignable` flag, and its permission labels. Each permission has a `permission`, `displayName`, `description`, and matching `policyAction`.

```ts
const { roles, permissions } = await client.subaccounts.listRoles();
const trader = roles.find((role) => role.role === "trader");
console.log(trader?.permissions, permissions.length);
```

### `getEffectivePermissions(input, options?)`

Returns the authenticated caller's `role`, effective `permissions`, and attached `subaccountPolicyId` for one subaccount. `subaccountPolicyId` is `""` when the subaccount uses the placeholder policy, on both this result and `Subaccount` reads.

```ts
const effective = await client.subaccounts.getEffectivePermissions({ subaccountId });
console.log(effective.role, effective.permissions, effective.subaccountPolicyId);
```

### `listMembers(input, options?)`

Returns the owner and delegated members of a subaccount, each with role and MFA enrollment status.

```ts
const members = await client.subaccounts.listMembers({ subaccountId });
for (const member of members) {
	console.log(member.username, member.role, member.mfaEnrolled);
}
```

### `updateMemberRole(input, options?)`

Changes an existing member's role without sending a new invitation.

```ts
await client.subaccounts.updateMemberRole({
	subaccountId,
	granteeAccountId,
	role: "viewer",
});
```

### `removeMember(input, options?)`

Revokes a member's delegated access by grantee account ID.

```ts
await client.subaccounts.removeMember({ subaccountId, granteeAccountId });
```

### `setMemberMfaRequirement(input, options?)`

Enables or disables the owner-controlled MFA gate for delegated interactive member actions on a subaccount.

```ts
await client.subaccounts.setMemberMfaRequirement({
	subaccountId,
	requireMemberMfa: true,
});
```

### `listEvents(input, options?)`

Returns the subaccount audit trail, newest first, with `{ events, nextPageToken }`. `limit` is capped at 200; page through with the returned token.

```ts
let pageToken = "";
do {
	const page = await client.subaccounts.listEvents({ subaccountId, limit: 200, pageToken });
	console.log(page.events.length);
	pageToken = page.nextPageToken;
} while (pageToken !== "");
```

Each `SubaccountEvent` carries `entityKind`, `eventAction`, `source`, `actorAccountId`, a parsed `payloadJson`, and a `createdAt` timestamp.

### `subscribe(input)`

Streams live subaccount updates over a private channel and returns an idempotent unsubscribe function. Takes an `accountId` plus `onEvent` (required) and optional `onOpen`, `onClose`, and `onError`. See the [realtime client reference](https://testnet.polyester.com/docs/sdk/typescript/reference/realtime) for the handler contract.

```ts
const unsubscribe = client.subaccounts.subscribe({
	accountId,
	onEvent: (subaccount) => console.log(subaccount.id, subaccount.status),
	onError: (ctx) => console.error(ctx.channel, ctx.error),
});

// later
unsubscribe();
```

### `subscribeApiKeys(input)`

Streams live API key updates for an account over a private channel, with the same handler contract and unsubscribe behavior as `subscribe`.

```ts
const unsubscribe = client.subaccounts.subscribeApiKeys({
	accountId,
	onEvent: (apiKey) => console.log(apiKey.keyId, apiKey.status),
});
```

## Policies

`client.subaccounts.policies` manages reusable permission templates: spot-market scopes, allowed actions, order limits, trading halts, review times, expiry, and policy locks. Attach a policy to a subaccount to cap what its members can do. Every method is authenticated.

| Method              | Summary                                                   |
| ------------------- | --------------------------------------------------------- |
| `list`              | List policy templates visible to the caller.              |
| `get`               | Fetch one policy by ID, or `null`.                        |
| `create`            | Create a policy, optionally attaching it to a subaccount. |
| `update`            | Patch selected fields on a policy.                        |
| `delete`            | Delete a policy that is not in use.                       |
| `apply`             | Attach a policy to a subaccount, or clear it.             |
| `subscribePolicies` | Stream policy updates.                                    |

### `policies.list(options?)`

Returns the policy templates visible to the caller, sorted by ascending policy ID.

```ts
const policies = await client.subaccounts.policies.list();
```

### `policies.get(input, options?)`

Fetches one policy template by base58 policy ID, returning `null` when none is found.

```ts
const policy = await client.subaccounts.policies.get({ policyId });
```

### `policies.create(input, options?)`

Creates a policy template from its scopes, actions, and limits, optionally attaching it to a target subaccount in the same request via `subaccountId`. Returns the created `SubaccountPolicy`.

```ts
const policy = await client.subaccounts.policies.create({
	name: "Read-only",
	spotMarketScope: "all",
	actions: ["read-spot", "read-balances", "read-internal-transfers", "read-address-book"],
	maxOrderSize: "0",
	subaccountId, // optional: attach on create
});
```

### `policies.update(input, options?)`

Patches an existing policy template. Pass `policyId`, `expectedRevision` from the policy you last read, and only the fields you want to change. If another writer updated the policy first, you get `RevisionConflictError`.

```ts
await client.catalog.ensureReady();
const symbolId = client.catalog.market.requireSymbolIdByPairSymbol("BTC-USDT");

await client.subaccounts.policies.update({
	policyId,
	expectedRevision: policy.revision,
	name: "Limited trader",
	spotMarketScope: "allowlist",
	spotMarkets: [{ symbolId }],
	actions: ["read-balances", "read-spot", "trade-spot"],
	maxOrderSize: "5000",
});
```

`maxOrderSize` is a decimal USDT string, with at most six decimal places. It is returned in the same form. `"0"` means no order-notional cap.

### `policies.delete(policyId, options?)`

Deletes a policy template. Only works when the policy is not attached to any subaccount.

```ts
await client.subaccounts.policies.delete(policyId);
```

### `policies.apply(input, options?)`

Attaches a policy to a subaccount. Passing `policyId: null` clears the explicit binding and restores the system read-only policy: `read-balances`, `read-internal-transfers`, `read-address-book`, and `read-spot`.

```ts
// Attach
await client.subaccounts.policies.apply({ subaccountId, policyId });

// Clear
await client.subaccounts.policies.apply({ subaccountId, policyId: null });
```

```ts
interface SubaccountPolicyApplyInput {
	subaccountId: string;
	policyId: string | null; // null detaches the policy
}
```

### `policies.subscribePolicies(input)`

Streams live policy updates over a private channel and returns an idempotent unsubscribe function. Takes an `accountId` plus the standard handler fields.

```ts
const unsubscribe = client.subaccounts.policies.subscribePolicies({
	accountId,
	onEvent: (policy) => console.log(policy.id, policy.name),
});
```

## Related

- [Accounts and balances guide](https://testnet.polyester.com/docs/sdk/typescript/guides/accounts-and-balances) for the task-oriented walkthrough.
- [Auth](https://testnet.polyester.com/docs/sdk/typescript/reference/auth) for `client.auth.createSubaccount` and account switching.
- [API keys](https://testnet.polyester.com/docs/sdk/typescript/reference/api-keys) for the keys returned by `get`.
- [MFA](https://testnet.polyester.com/docs/sdk/typescript/reference/mfa) for the member MFA requirement gate.
- [Errors](https://testnet.polyester.com/docs/sdk/typescript/reference/errors) for validation and lookup error types.
