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

# Advanced execute

> Understand ExecutionAccount boundaries, atomic return-to-pool, and lower-level execute helpers.

This page covers the edge cases around `execute()`: what the ExecutionAccount
can do, how assets are returned to the private pool atomically, and what the
advanced SDK exposes for custom orchestration and follow-up calls.

## ExecutionAccount boundary

An `execute()` session is a single sponsored ERC-4337 UserOperation built around
the ExecutionAccount. Depending on the request it contains:

* Optional private withdrawals from the user's Unlink balance into the
  ExecutionAccount (`withdrawFromPool`, one entry per token).
* The user `calls`, run as the `userCalls` section of the plan.
* An optional atomic `returnToPool` deposit that returns ERC-20 tokens from the
  ExecutionAccount back into the user's private Unlink balance.

The account runs all three as one typed plan
(`executeUnlinkPlan(withdrawFromPool, userCalls, returnToPool)`). The withdrawals,
the calls, and the deposit back into the pool either all succeed or all revert
together — there is no separate return transaction and no partial-settlement
state.

The ExecutionAccount is a smart account, not an EOA. The owner key signs an
ExecutionIntent for ERC-4337 validation. It does not send normal EVM
transactions directly from the ExecutionAccount.

## Session shape: funded vs unfunded

The number of signatures and round trips depends on whether the session
withdraws from the pool.

* **Funded session** (`withdrawFromPool` present) — funding mode
  `private_withdrawal`. Three steps: `prepare` → `finalize` → `submit`. Prepare
  returns one withdrawal signing request **per `withdrawFromPool` entry** (one
  private spend proof per token); the user signs each; `finalize` proves the
  withdrawals, materializes the atomic `withdrawToExecutionAccount` funding
  section, and returns the final ExecutionIntent; the user signs that intent and
  `submit` broadcasts the UserOperation. The ExecutionIntent is null until
  finalize because the plan (and its `planHash`) is not complete until the
  funding section is materialized.
* **Unfunded session** (`withdrawFromPool` omitted, including every
  `executeAccountCall`) — funding mode `existing_execution_account`. Two steps:
  `prepare` → `submit`. Prepare returns the ExecutionIntent directly because no
  funding section has to be materialized.

The high-level `client.execute()` / `client.executeAccountCall()` helpers run
the whole sequence for you, including the withdrawal and intent signatures. The
advanced helpers below expose the individual steps.

## Advanced SDK surface

The `@unlink-xyz/sdk/advanced` subpath exports lower-level helpers for custom
orchestration:

```ts theme={null}
import {
  createExecutionAccountClient,
  executeAccountCall,
  finalizeExecute,
  getExecuteSession,
  pollExecuteStatus,
  prepareExecute,
  prepareExecuteAccountCall,
  reserveExecutionAccount,
  submitExecute,
  submitExecuteAccountCall,
} from "@unlink-xyz/sdk/advanced";
```

`prepareExecute` / `finalizeExecute` / `submitExecute` are the funded
private-withdrawal execute-session steps. Direct `prepareExecute` callers pass an
account candidate (`accountPolicy` plus `executionAccount.slotIndex`, address,
owner, and initCode); `/execute/prepare` is the atomic bind-or-conflict boundary.
`execute()` handles the normal discovery, local derivation, and retry loop for
you. `prepareExecuteAccountCall` / `submitExecuteAccountCall` prepare and
submit a sponsored UserOperation from an ExecutionAccount without preparing a
withdrawal. The prepare request may include factory initCode for the account's
first bind/deploy; the durable account binding is completed on submit after the
backend verifies the owner's `ExecutionIntent` signature. Direct low-level
callers that use first-bind evidence in prepare must send the same factory
initCode to `submitExecuteAccountCall`. `executeAccountCall` is the high-level
helper that performs the same account-call flow for you. `finalizeExecute`
applies only to funded sessions; unfunded sessions submit the intent returned by
prepare directly.

`reserveExecutionAccount` is still exported for explicit discovery/reservation
tools and legacy custom flows. The high-level private-withdrawal `execute()`
path does not call it; it lists accounts for an environment, derives the
candidate account from the returned allocation hint, then prepares that
candidate.

By default, `execute()` and `executeAccountCall()` resolve when the backend
reports `execution.confirmation_status: "confirmed"`: the handleOps
receipt/effects required by the session have been observed and are visible to
API reads. Pass `waitUntil: "processed"` (or `"finalized"`) if your integration
needs the durable terminal execution-session state before continuing.

<Warning>
  The withdrawless account-call flow does not create a pool withdrawal, and it
  is not a `withdraw(0)` workaround. Cold first use is supported only for the
  verified ExecutionAccount factory initCode derived by the SDK.
</Warning>

Pass an `evm` provider when constructing the user client to enable the SDK's
client-side deployed-code check for warm account calls. The backend always
performs the authoritative factory/initCode derivation checks before preparing a
withdrawless account-call session, and requires deployed code only when the
request is reusing an account without first-bind initCode evidence.

## Return assets with returnToPool

Pass `returnToPool` in the same `execute()` call when the batch leaves ERC-20
tokens in the ExecutionAccount and those tokens should go back into the user's
private balance.

The return is atomic and typed. `returnToPool` is the plan's `returnToPool`
section, grouped by token; the ExecutionAccount constructs each token group's
`approve` → `depositFromExecutionAccount` → allowance-reset on-chain, inside the
same UserOperation as the funding withdrawals and your calls. The ExecutionAccount
is the depositor and the pool authorizes it directly, so there is no Permit2
signature, nonce, or deadline to manage — and no separate deposit transaction that
can fail on its own.

```ts theme={null}
const amount = 1_000_000_000_000_000_000n;

const result = await client.execute({
  withdrawFromPool: [{ token, amount: amount.toString() }],
  calls: [
    // protocol calls first, if any
  ],
  returnToPool: [{ token, amount: amount.toString() }],
});
```

Your batch must leave at least the requested amount of each `returnToPool` token
in the ExecutionAccount when the batch reaches the deposit step. If the batch
swaps into a different ERC-20, set the `returnToPool` `token` to whatever token
remains in the account.

Funding, calls, and return-to-pool are three independently-capped plan sections,
not one combined batch: at most 16 `calls`, at most 8 `withdrawFromPool` tokens,
and at most 8 `returnToPool` token groups with at most 4 output notes per token.
Return-to-pool groups do not consume the 16-call budget. The structural caps sit
alongside a per-network sponsored gas budget checked at prepare, so a plan
stacking several sections near their caps can receive a gas-budget rejection
even with every section within bounds. Each token group can mix
fixed `amount` outputs with one `sweep` output that re-shields the runtime
remainder (`sweep: true` + required `max_total`, optional `min_total` floor and
`baseline`). `recipient` is reserved for future use; v1 defaults every output to
the authenticated Unlink address and rejects a supplied recipient.

## Follow-up calls without withdrawal

If a previous execute session completed without `returnToPool`, those assets can
remain in the ExecutionAccount. Use `client.executeAccountCall` to run another
call from the same account index without moving funds through the pool first.
The same recovery path applies when a known terminal failure such as
`user_op_reverted` leaves tokens in the ExecutionAccount.

This still uses the ExecutionAccount owner signature and Unlink's sponsored
ERC-4337 path. The session has funding mode `existing_execution_account`, and its
withdrawal fields are empty arrays: `withdraw_from_pool: []` and
`withdrawal_tx_ids: []`.

```ts theme={null}
const accountIndex = previousResult.execution.account_index;
const stuckAmount = 1_000_000_000_000_000_000n;

await client.executeAccountCall({
  accountIndex,
  calls: [
    {
      target: stuckToken,
      value: "0",
      data: "0x...", // optional follow-up calldata from the ExecutionAccount
    },
  ],
  returnToPool: [{ token: stuckToken, amount: stuckAmount.toString() }],
});
```

You can also use it for a non-deposit follow-up call, as long as the target
calldata can execute from the ExecutionAccount and does not need a new private
withdrawal. When `returnToPool` is omitted, `calls` must be non-empty.

## Public EOA deposit fallback

`depositWithApproval()` deposits from the connected EVM wallet, not from the
ExecutionAccount. If atomic `returnToPool` is not suitable for a flow, transfer
the tokens from the ExecutionAccount to the user's EOA in a follow-up
`executeAccountCall()`, then call `depositWithApproval()` from that EOA.

<Warning>
  This fallback is public. The ERC-20 transfer from the ExecutionAccount to the
  EOA and the later deposit source wallet are visible on-chain.
</Warning>

```ts theme={null}
import { account, createUnlinkClient, evm } from "@unlink-xyz/sdk/browser";
import { encodeFunctionData, erc20Abi } from "viem";

const evmProvider = evm.fromEip1193({ provider: window.ethereum });

const client = createUnlinkClient({
  environment: "base-sepolia",
  account: account.fromMnemonic({ mnemonic }),
  evm: evmProvider,
});

await client.ensureRegistered();

const token = "0xTokenAddress";
const protocol = "0xProtocolAddress";
const firstAmount = 1_000_000_000_000_000_000n;

const first = await client.execute({
  withdrawFromPool: [{ token, amount: firstAmount.toString() }],
  calls: [
    {
      target: token,
      value: "0",
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: "approve",
        args: [protocol, firstAmount],
      }),
    },
    {
      target: protocol,
      value: "0",
      data: "0x...", // initial protocol calldata
    },
  ],
  waitUntil: "processed",
});

if (first.status !== "completed") {
  throw new Error(`execute ended with status ${first.status}`);
}

const accountIndex = first.execution.account_index;
const recipientEoa = await evmProvider.getAddress();
const amountToDeposit = 500_000_000_000_000_000n;

const followUp = await client.executeAccountCall({
  accountIndex,
  calls: [
    {
      target: protocol,
      value: "0",
      data: "0x...", // optional follow-up calldata from the ExecutionAccount
    },
    {
      target: token,
      value: "0",
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: "transfer",
        args: [recipientEoa, amountToDeposit],
      }),
    },
  ],
  waitUntil: "processed",
});

if (followUp.status !== "completed") {
  throw new Error(`account call ended with status ${followUp.status}`);
}

const deposit = await client.depositWithApproval({
  token,
  amount: amountToDeposit.toString(),
});

const confirmed = await deposit.wait();
console.log(confirmed.confirmationStatus);
```

The pool, circuits, and contracts still require positive note amounts for real
withdrawals and outputs. A zero-amount withdrawal is not the supported escape
hatch.
