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

# Prepare and sign transactions

> Build an unsigned plan, verify it against your own intent and execute a supported Base swap with your signer.

Otto's free `otto_prepare_*` tools return unsigned action plans. They never sign or submit. Each plan binds an account to a payload, checks and expiry. Your signer decides whether to execute it.

The published [otto-execute@0.1.3](https://www.npmjs.com/package/otto-execute) client verifies and executes the **LI.FI Base EOA swap** shown below. Other constructors need their corresponding signer integration; this version has no bridge, Hyperliquid or Polymarket submission adapter. Coinbase delegation is a separate authenticated path, described at the end of this page.

**Hyperliquid order construction through Otto is retired.** Do not use `otto_prepare_perp_order` or a cached constructor listing. Hyperliquid market research remains; existing-account recovery is described in [Hyperliquid retirement & recovery](/acp-swarm/hyperliquid).

## Supported constructors

| Tool | Builds |
| - | - |
| `otto_prepare_swap` | Prepare an Otto-attributed LI.FI swap |
| `otto_prepare_bridge` | Prepare an Otto-attributed LI.FI cross-chain USDC bridge |
| `otto_prepare_polymarket_order` | Prepare an Otto-attributed Polymarket CLOB limit order |
| `otto_prepare_stock_buy` | Prepare a tokenized-stock buy (Coinbase B20 on Base) as an unsigned swap pair |
| `otto_prepare_yield_deposit` | Prepare a Yield Copilot vault deposit (Morpho USDC on Base) as an unsigned swap pair |
| `otto_prepare_yield_withdraw` | Prepare a Yield Copilot vault withdraw (Morpho USDC on Base) as a delegable swap pair |

Use `tools/list` on the [hosted MCP](/acp-swarm/otto-intel-mcp) for complete JSON schemas. Swap and bridge amounts are strings in atomic token units; Polymarket price/shares use decimal strings. Stock and yield tools require the executable registry ID and commitment returned by the relevant registry. Listing a constructor does not establish that every app action is enabled.

## Prepare a Base swap

This example prepares **1 USDC to WETH** for an EOA you control. Preparation and verification move no funds. It needs Bash, Node 22+, and a Base address. USDC has 6 decimals; WETH has 18.

```bash theme={null}
mkdir otto-swap
cd otto-swap
umask 077
npm init -y
npm install --save-exact otto-execute@0.1.3 @modelcontextprotocol/sdk@1.29.0
read -r -p 'Your Base EOA address: ' OTTO_ACCOUNT
read -r -p 'Minimum WETH output in atomic units: ' OTTO_MIN_WETH_ATOMIC
export OTTO_ACCOUNT OTTO_MIN_WETH_ATOMIC
```

Choose your minimum output independently, using a price source you trust. For units only, `200000000000000` means 0.0002 WETH; this is not a current quote or a recommended floor.

Save this as `prepare.mjs`. It records **your intent before requesting a plan**, then saves the complete MCP response and parsed envelope:

```javascript theme={null}
import { writeFileSync } from 'node:fs';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const address = process.env.OTTO_ACCOUNT;
const minimum = process.env.OTTO_MIN_WETH_ATOMIC;
if (!/^0x[0-9a-fA-F]{40}$/.test(address ?? '')) throw new Error('Invalid address');
if (!/^[1-9][0-9]*$/.test(minimum ?? '')) throw new Error('Minimum must be positive atomic units');
const intent = {
  fromToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  toToken: '0x4200000000000000000000000000000000000006',
  fromAmount: '1000000',
  minAmountOut: minimum,
};
const input = {
  accountProfile: { kind: 'eoa', address, chainId: 8453 },
  chainId: 8453,
  fromToken: intent.fromToken,
  toToken: intent.toToken,
  fromAmount: intent.fromAmount,
  slippage: { maxBps: 50, minAmountOut: intent.minAmountOut },
  client_ref: 'my-first-otto-swap',
};
const save = (path, value) => writeFileSync(path, JSON.stringify(value, null, 2), { mode: 0o600 });
save('intent.json', intent);
save('prepare.json', input);
const client = new Client({ name: 'otto-swap-example', version: '1.0.0' });
try {
  await client.connect(new StreamableHTTPClientTransport(
    new URL('https://mcp.ottoai.services/mcp'),
  ));
  const result = await client.callTool({ name: 'otto_prepare_swap', arguments: input });
  save('envelope.mcp.json', result);
  const text = result.content.find(part => part.type === 'text')?.text;
  if (result.isError || !text) throw new Error('Constructor refused; inspect envelope.mcp.json');
  save('envelope.json', JSON.parse(text));
} finally {
  await client.close();
}
```

Run it, then verify the result against the saved intent and request:

```bash theme={null}
node prepare.mjs
npx --no-install otto-execute verify \
  --envelope envelope.json --intent intent.json \
  --prepare-input prepare.json --account "$OTTO_ACCOUNT"
```

A successful verification prints the account, exact input, minimum output, decoded fee distributions and remaining validity. Check those values. Never derive `intent.json` from Otto's returned plan: that would compare the plan with itself.

The hosted envelope expires after five minutes. A refusal has exit code 2 and a named code: `STALE_ARTIFACT` means prepare afresh; account, intent or payload mismatches need investigation. Do not remove a check to make the plan pass.

## Sign and submit

The signing EOA needs Base USDC for the swap and Base ETH for gas. It can differ from the wallet paying for intelligence. Load its key locally using a secret manager or a hidden terminal prompt:

```bash theme={null}
read -r -s -p 'Swap wallet private key (hidden): ' OTTO_EXECUTE_PRIVATE_KEY
printf '\n'
export OTTO_EXECUTE_PRIVATE_KEY

npx --no-install otto-execute sign \
  --envelope envelope.json --intent intent.json \
  --prepare-input prepare.json > signed-dry-run.json
```

`sign` signs and decodes the ordered steps without broadcasting. Keep the output private: it contains signed transaction material.

The following command **broadcasts on Base mainnet**. Use a current plan and run it only after deciding to execute this trade:

```bash theme={null}
npx --no-install otto-execute submit \
  --envelope envelope.json --intent intent.json \
  --prepare-input prepare.json --i-understand-mainnet > execution-result.json
unset OTTO_EXECUTE_PRIVATE_KEY
```

The CLI checks freshness before each signature. Preserve transaction hashes and inspect chain status after an interruption before preparing a replacement trade. The constructor does not guarantee single execution: account nonces or venue order identifiers enforce the applicable replay boundary. An interrupted process does not prove that nothing was submitted.

## What the envelope proves

| Field | Meaning |
| - | - |
| `account_binding` | The supplied account and its sender/receiver roles |
| `payload` | Ordered EVM steps, or the venue-specific unsigned action |
| `assertions` | Constructor checks and their results |
| `valid_until` | Expiry committed into the artifact digest |
| `submission.payload_digest` | Commitment used to detect an altered plan |
| `submission.adapter_owned_fields` | Values the signer supplies, such as nonce, gas and serialization |
| `required_capabilities` | Signing capabilities needed to execute |
| `fee_attribution` | Fee mechanism and configuration; LI.FI fee legs can be decoded before signing |
| `replay_disclosure` | Construction aid, not a single-execution guarantee |

<Accordion title="Base swap verification checks">
  An `otto_prepare_swap` envelope carries twelve assertions, each reported with
  its result in the response. A check that does not hold **refuses the call by
  name** rather than returning a plan.

  | Assertion | What it establishes |
  | - | - |
  | `lifi_integrator_exact` | The decoded calldata carries Otto's exact integrator string and no other. |
  | `lifi_fee_recipient_exact` | Every distribution recipient in the decoded body is one of the reviewed, versioned set. An unrecognized recipient is refused. |
  | `lifi_fee_amount_exact` | Each distribution carries exactly the configured amount for this gross input. |
  | `lifi_fee_recipient_set_versioned` | The distribution body names the complete versioned recipient set, once each — no duplicates, none missing. |
  | `lifi_fee_aggregate_cap` | The sum of all distribution legs is at or below the configured cap for this gross input. |
  | `lifi_receiver_account_bound` | The receiver decoded out of the calldata, and the quote's sender and receiver, are the address you supplied. |
  | `lifi_min_out_slippage_bound` | The minimum output in the calldata equals the quoted minimum and sits at or above your slippage floor, and at or above your own `minAmountOut` when you set one. |
  | `lifi_token_amount_bound` | Every deposit leg spends only your input token and they total exactly your input amount; the final leg's output token is the one you asked for. |
  | `lifi_targets_value_bound` | The approval target and the call target are both the reviewed LI.FI Diamond, and the call moves zero native value. |
  | `payload_digest_committed` | The returned plan is hashed and the digest travels with it in `submission.payload_digest`. |
  | `erc20_allowance_reset_ordered` | The plan clears the allowance to zero before setting the new one, so it is executable from any prior allowance state, including on approval-race-guarded tokens. |
  | `artifact_expiry_committed` | `valid_until` is committed into the artifact digest, so a re-dated plan no longer matches. |
</Accordion>

## Existing Coinbase trading permissions

[Legacy wallet recovery](https://useotto.xyz/app/legacy-wallet-recovery) lets you review and stop an existing Model B trading permission for a **separate Coinbase end-user account**. New permission creation and replacement are retired. Public MCP access alone does not authorize protected submission.

An existing permission's recorded expiry and cap record remain part of its validation; neither authorizes new delegated trading. Provider limits are not cumulative budgets or maximum-loss limits. `otto_delegation_fence_status` reports `mint_enabled: false` for new Model B trading-permission setup, while the provider fence may remain present for existing grants. A qualifying withdrawal also requires confirmed ruleset-v2 withdrawal consent.

New delegated trading is retired, including swaps, stock buys and yield deposits under existing grants — `otto_submit_under_delegation` returns `DELEGATION_TRADING_RETIRED` for all three. **Only qualified vault withdrawals remain available** under an existing permission; the share input, USDC output, reviewed spender and fence v2 checks are mandatory. It does not cover stock sells, bridges, Hyperliquid or Polymarket. Retail Stocks uses connected-wallet signatures. Withdrawals redeem vault shares to USDC **inside the Coinbase account**; their share-quantity and recipient checks are separate from the per-swap spend cap.

Revocation removes authority; it does not redeem positions or move assets. A permission predating withdrawal consent cannot authorize that withdrawal, and the retired setup cannot upgrade it. Check the available direct exit path in [Moving funds out](/account-and-settings/removing-funds); do not assume every vault exit works without the required existing permission.

<Accordion title="Review or stop an existing permission from the published CLI">
  Version 0.1.3 supports email OTP status and revocation with these optional dependencies:

  ```bash theme={null}
  npm install --save-exact @coinbase/cdp-core@0.0.120 react@19.2.0
  unset OTTO_EXECUTE_PRIVATE_KEY
  npx --no-install otto-execute delegation status --email you@example.com
  npx --no-install otto-execute delegation revoke --email you@example.com
  ```

  Use your own email. Enter the OTP interactively. This flow runs without a wallet private key or Otto service credentials in its environment. The older `delegate` command may still be installed, but the retirement server refuses new reservations. Its Node OTP bridge uses Coinbase's web SDK and is not a Coinbase-supported Node integration.

  Require read-back and durable confirmation after revocation. `REVOKE_NOT_DURABLE` means confirmation is incomplete and revocation should be retried. These commands manage permission; `submit` remains the EOA path above. Do not request or distribute Otto's private delegation header.
</Accordion>


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