# Auto-withdraw

Manage an account's scheduled auto-withdraw rule and read the runs that paid it out.

`client.autoWithdraw` manages the scheduled auto-withdraw rule of the root account or a subaccount. On each UTC slot of its schedule, a rule pays each listed asset's balance, in kind, to a destination on the account's external withdraw whitelist. Every method is authenticated and account-scoped, so each input accepts an optional `account` field (`"main"`, `"active"`, or `{ subaccountId }`). See [Account scoping](https://testnet.polyester.com/docs/sdk/typescript/guides/accounts-and-balances) for how the default resolves.

API keys can read rules and runs when their policy grants `read-auto-withdraw` (or `manage-auto-withdraw`), and only for their own account; otherwise they throw `PermissionError`. Changing a rule needs a session login with withdraw rights and a fresh single-use step-up proof passed as `options.stepUpToken`. Without one, `setRule` and `disableRule` throw `StepUpRequiredError` or `SessionElevationRequiredError`; check `isFreshStepUpRequiredError` before retrying with a new token. See [MFA](https://testnet.polyester.com/docs/sdk/typescript/reference/mfa).

## Methods

| Method        | Summary                                                          |
| ------------- | ---------------------------------------------------------------- |
| `getRule`     | Read the account's latest rule version, or `null`.               |
| `setRule`     | Create the rule, or replace its latest version.                  |
| `disableRule` | Stop the rule by writing a disabled version.                     |
| `listRuns`    | Page through the account's runs and their payouts, newest first. |

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

Returns the account's latest `AutoWithdrawRule`, or `null` when the account never had a rule.

```ts
const rule = await client.autoWithdraw.getRule();
console.log(rule?.version, rule?.enabled, rule?.schedule.cadence);
```

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

Creates the account's rule, or replaces its latest version, and returns the new version. Pass the version you are replacing as `expectedVersion`: `0` when the account has no rule yet, otherwise its latest `version`.

```ts
const current = await client.autoWithdraw.getRule();

const rule = await client.autoWithdraw.setRule(
	{
		expectedVersion: current?.version ?? 0,
		schedule: { cadence: "weekly", weekday: "friday", minuteOfDay: 17 * 60 },
		destinations: [
			{
				assetId: 2,
				destinationChainId: 1,
				destinationAddress: "0xabc...",
				minAmount: "100",
				maxAmount: "5000",
			},
		],
		minUsdValue: "50",
	},
	{ stepUpToken }
);
```

> **A stale version throws RevisionConflictError**
>
> If the rule changed since you read it, `setRule` and `disableRule` throw `RevisionConflictError`. Refetch the rule and ask the user to review the change; do not retry the same write automatically.

#### `SetAutoWithdrawRuleInput`

| Field             | Type                             | Required | Notes                                                            |
| ----------------- | -------------------------------- | -------- | ---------------------------------------------------------------- |
| `expectedVersion` | `number`                         | yes      | The version this change replaces; `0` when there is no rule yet. |
| `schedule`        | `AutoWithdrawScheduleInput`      | yes      | The UTC run slot. See below.                                     |
| `destinations`    | `AutoWithdrawDestinationInput[]` | yes      | 1 to 32, at most one per asset.                                  |
| `minUsdValue`     | decimal string \| `null`         | no       | Least USD value one payout pays. Omit or `null` for no minimum.  |
| `account`         | `AccountScope`                   | no       | Scope override.                                                  |

`schedule` is tagged by `cadence`, and each cadence takes only its own fields. Any other field throws `ValidationError`. All times are UTC.

| `cadence`    | Fields                                 |
| ------------ | -------------------------------------- |
| `"hourly"`   | `minuteOfHour` (0 to 59)               |
| `"daily"`    | `minuteOfDay` (0 to 1439)              |
| `"weekly"`   | `weekday`, `minuteOfDay`               |
| `"biweekly"` | `weekday`, `minuteOfDay`, `anchorDate` |
| `"monthly"`  | `dayOfMonth` (1 to 31), `minuteOfDay`  |

`minuteOfDay` counts minutes after 00:00 UTC. `weekday` is `"monday"` through `"sunday"`. A `dayOfMonth` past the end of a shorter month runs on its last day. `anchorDate` is a `YYYY-MM-DD` UTC date in a week the biweekly rule runs; weeks start on Monday, and the returned rule carries that week's Monday.

Each destination pays one asset to one external-chain address:

| Field                | Type           | Notes                                                             |
| -------------------- | -------------- | ----------------------------------------------------------------- |
| `assetId`            | `number`       | Ledger asset paid.                                                |
| `destinationChainId` | `number`       | External destination chain ID.                                    |
| `destinationAddress` | `string`       | Must be on the account's external withdraw whitelist.             |
| `minAmount`          | decimal string | A run pays the asset only once the payable amount reaches this.   |
| `maxAmount`          | decimal string | The most one run pays. Greater than `0` and at least `minAmount`. |

Amounts take up to 18 decimal places. `minAmount` must be at least the route's withdrawal minimum. A balance below the minimums stays on the account for a later run, and a balance above `maxAmount` pays `maxAmount` and keeps the rest. With `minUsdValue` set, each run values each asset at the current index price; when no current price is available, that asset is skipped with reason `price_unavailable`.

A schedule the backend refuses fails with `detail.service === "withdraw"` and `detail.code === "INVALID_AUTO_WITHDRAW_SCHEDULE"`.

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

Disables the rule and returns the disabled version, which has `enabled: false` and no destinations. Pass the latest `version` as `expectedVersion`. Needs a fresh `stepUpToken`.

```ts
await client.autoWithdraw.disableRule({ expectedVersion: rule.version }, { stepUpToken });
```

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

Returns `{ runs, nextPageToken }`, newest first. `limit` is 0 through 100; `0` or omitted uses the server default of 20. Pass the previous `nextPageToken` as `pageToken` to continue; an empty `nextPageToken` means there are no more runs.

```ts
let pageToken = "";
do {
	const page = await client.autoWithdraw.listRuns({ pageToken, limit: 50 });
	for (const run of page.runs) console.log(run.runSeq, run.state, run.steps.length);
	pageToken = page.nextPageToken;
} while (pageToken !== "");
```

## Shapes

Amounts are decimal strings and times are epoch milliseconds.

```ts
import type { AutoWithdrawRule, AutoWithdrawRun, AutoWithdrawStep } from "@polyester/sdk";

interface AutoWithdrawRule {
	subaccountId: string; // public subaccount ID, "" for the root account
	version: number; // starts at 1; each change or disable creates the next version
	enabled: boolean;
	schedule: AutoWithdrawSchedule; // { cadence: "unspecified" } for a cadence this build does not know
	destinations: AutoWithdrawDestination[]; // ordered by assetId
	createdAt: number;
	pause: AutoWithdrawPause | null; // set when the rule paused itself
	minUsdValue: string | null; // null when the rule has no USD minimum
}

interface AutoWithdrawRun {
	runSeq: string; // the account's run number, one higher per run
	ruleVersion: number; // the rule version the run executes
	scheduledFor: number;
	state:
		| "planned"
		| "paying"
		| "completed"
		| "partially_failed"
		| "failed"
		| "skipped"
		| "unspecified";
	reason: AutoWithdrawRunReason | null; // why the run was skipped
	steps: AutoWithdrawStep[]; // one per destination, empty while planned
	createdAt: number;
	finishedAt: number | null; // null while in progress
}

interface AutoWithdrawStep {
	assetId: number;
	destinationChainId: number;
	destinationAddress: string;
	available: string; // available balance when the run started
	amount: string; // what the step pays; "0" when skipped
	valueUsd: string | null; // null without a USD minimum or a price
	state:
		"pending" | "submitting" | "submitted" | "completed" | "failed" | "skipped" | "unspecified";
	reason: AutoWithdrawStepReason | null; // why the step was skipped or failed
	intentId: string | null; // set once the withdrawal is accepted
	txHash: string | null; // set once the withdrawal completed
}
```

Only the latest rule version runs. A run in progress keeps the version it started with but pays nothing further once that version is no longer the latest. A step's `intentId` follows the same [lifecycle](https://testnet.polyester.com/docs/sdk/typescript/reference/lifecycle) as a user withdrawal.

`pause` is `{ assetId, reason, pausedAt }`. Writing a new version resumes the rule. Reasons are strings, and reasons newer than this SDK build pass through unchanged:

| Field | Known reasons                                                                                                                                                                            |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pause | `destination_not_whitelisted`, `destination_invalid`, `authority_revoked` (the person who set the rule may no longer change it or withdraw from the account)                             |
| Run   | `nothing_to_pay`, `price_unavailable`, `rule_superseded`                                                                                                                                 |
| Step  | `below_minimum`, `price_unavailable`, `rule_superseded`, `destination_not_whitelisted`, `authority_revoked`, `gateway_capacity_insufficient`, `withdrawal_released`, `withdrawal_failed` |

For `price_unavailable` and `gateway_capacity_insufficient`, the funds stay on the account for a later run.

## Related

- [Withdrawals](https://testnet.polyester.com/docs/sdk/typescript/reference/withdrawals) for one-off signed withdrawals.
- [Address book](https://testnet.polyester.com/docs/sdk/typescript/reference/address-book) for the external withdraw whitelist.
- [MFA](https://testnet.polyester.com/docs/sdk/typescript/reference/mfa) for obtaining a `stepUpToken`.
- [Lifecycle](https://testnet.polyester.com/docs/sdk/typescript/reference/lifecycle) to follow each payout's withdrawal.
