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

# End-to-end transaction flow

> Prepare, authorize, submit, and track a multichain transaction.

Every multichain transaction follows the same lifecycle, but each client exposes different boundaries. Embedded wallets manage preparation, authorization, and submission through `sendIntent()`. The custom signer SDK exposes each step. REST integrations call the endpoints directly.

## Transaction lifecycle

1. Define the target chain, required tokens, destination calls, and source constraints.
2. Prepare a route and review its inputs, outputs, fees, and expiry.
3. Complete any token requirements for the account type.
4. Authorize the exact prepared route.
5. Submit the signed intent.
6. Track the intent until it completes or fails.

An accepted submission is not an onchain success. Keep the intent ID so you can resume status checks without creating another transaction.

## Follow the flow

Reuse the chains, token addresses, `amount`, and `calls` from [Send a transaction](/transactions/multichain/send-a-transaction).

<Tabs>
  <Tab title="Embedded wallet">
    Use the initialized `oneAuth` client and authenticated `accountAddress` from [Accounts](/wallets/embedded-wallets/accounts).

    <Steps>
      <Step title="Review, authorize, and submit">
        ```ts {8-9} theme={null}
        const result = await oneAuth.sendIntent({
          accountAddress,
          targetChain: arbitrum.id,
          sourceChainId: base.id,
          sourceAssets: [usdcOnBase],
          tokenRequests: [{ token: usdcOnArbitrum, amount }],
          calls: [...calls],
          closeOn: "completed",
          waitForHash: true,
        });
        ```

        Embedded wallets prepare the route, handle their smart-account token requirements, request passkey approval, and submit the intent. They do not expose the custom signer SDK's ranked quotes or separate signing and submission methods.

        Sponsorship is required by default. `closeOn: "completed"` waits for completion; `waitForHash: true` separately requests the onchain transaction hash. Add [clear signing](/wallets/embedded-wallets/clear-signing) when your application needs prepared-action review.

        Save any non-empty `result.intentId` before handling errors. `STATUS_TIMEOUT`, `HASH_TIMEOUT`, or a dialog cancellation with an intent ID can occur after submission. They are not reasons to send a replacement intent. If the submission outcome is uncertain and no ID was returned, reconcile intent history before starting another.
      </Step>

      <Step title="Check or resume execution">
        Use `getIntentStatus()` when you have an intent ID. Check for a lookup error before interpreting the status:

        ```ts {3,8,12,18} theme={null}
        async function waitForWalletCompletion(intentId: string) {
          for (let attempt = 0; attempt < 60; attempt++) {
            const latest = await oneAuth.getIntentStatus(intentId);

            if (latest.error) {
              console.error(latest.error.code, latest.error.message);
            } else if (
              latest.status === "completed" ||
              latest.status === "failed" ||
              latest.status === "expired"
            ) {
              return latest;
            }

            await new Promise((resolve) => setTimeout(resolve, 2_000));
          }

          return undefined;
        }

        if (!result.intentId) {
          throw new Error(result.error?.message ?? "No intent ID returned");
        }

        const latest = await waitForWalletCompletion(result.intentId);

        if (!latest) {
          console.log("Check this intent again later", result.intentId);
        } else if (latest.status === "completed") {
          console.log(latest.intentId, latest.transactionHash);
        } else {
          console.error("Intent failed", latest.intentId, latest.status);
        }
        ```

        The helper stops after 60 status requests. `undefined` means the outcome is still unknown, not failed; retain the ID for a later check. Stopping your poll does not cancel the intent.

        In `@rhinestone/1auth@0.10.1`, `getIntentStatus()` returns `success: false` for valid non-terminal statuses. It also returns a synthetic `status: "failed"` with `STATUS_FAILED` or `NETWORK_ERROR` when the lookup fails. An error-free `failed` or `expired` status, by contrast, is an execution outcome.
      </Step>
    </Steps>

    ### Recover from errors

    | Signal                                                    | Action                                                                                       |
    | --------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
    | `STATUS_TIMEOUT` or `HASH_TIMEOUT`                        | Keep the intent ID and query status. A waiting timeout does not prove execution failure.     |
    | `STATUS_FAILED` or `NETWORK_ERROR` during a status lookup | Retry the status read, not the transaction. Check credentials if failures persist.           |
    | `USER_CANCELLED` or `USER_REJECTED`                       | Do not retry automatically. Query any existing intent ID before asking for another approval. |
    | Error-free `failed` or `expired` status                   | Inspect the failure before asking the user to authorize another intent.                      |

    Local statuses such as `submitted`, `claimed`, `preconfirmed`, `filled`, and `completed` are not the custom signer SDK's uppercase statuses. Record `error.details?.traceId` when present; do not show raw diagnostics or simulation URLs to users.
  </Tab>

  <Tab title="Custom signer">
    Use the account and backend SDK instance from the [Custom signer quickstart](/wallets/custom-signer/quickstart).

    <Steps>
      <Step title="Prepare and inspect">
        ```ts theme={null}
        const prepared = await account.prepareTransaction({
          sourceChains: [base],
          sourceAssets: [usdcOnBase],
          targetChain: arbitrum,
          calls: [...calls],
          tokenRequests: [{ address: usdcOnArbitrum, amount }],
        });

        const quote = prepared.quotes.best;
        console.log(quote.cost.input, quote.cost.output, quote.cost.fees);
        console.log(quote.expiresAt, prepared.quotes.traceId);
        ```

        `prepared.quotes.best` is the recommended route. To choose another, select it from `prepared.quotes.all`. Show the user what the selected route spends, delivers, and charges before signing. `expiresAt` is a Unix timestamp in seconds.

        Smart accounts handle approvals and wrapping in the route. A plain EOA must first [complete the selected quote's token requirements](#complete-token-requirements).
      </Step>

      <Step title="Sign the selected route">
        ```ts {5} theme={null}
        if (Date.now() >= quote.expiresAt * 1_000) {
          throw new Error("Quote expired; prepare and review a new route");
        }

        const signed = await account.signTransaction(prepared, {
          intentId: quote.intentId,
        });
        ```

        The account signs the required origin, destination, and target-execution data using its configured owners or session. Never combine a signature from one route with another route, or feed manually reconstructed REST sign data into this SDK flow.
      </Step>

      <Step title="Submit and wait">
        ```ts theme={null}
        import { isExecutionError } from "@rhinestone/sdk/errors";

        if (Date.now() >= quote.expiresAt * 1_000) {
          throw new Error("Quote expired; prepare and sign a new route");
        }

        const submitted = await account.submitTransaction(signed);
        console.log(submitted.id, submitted.traceId);

        try {
          const status = await account.waitForExecution(submitted);
          console.log(status.status, status.operations);
        } catch (error) {
          if (error instanceof Error && isExecutionError(error)) {
            console.error(error.message, error.context, submitted.traceId);
          }
          throw error;
        }
        ```

        `submitTransaction` returns a handle after acceptance. Save its `id` and `traceId`. `waitForExecution` polls until completion and throws when execution fails; it has no application deadline. To resume a status check, call `rhinestone.getIntentStatus(submitted.id)` with the SDK instance from the quickstart.
      </Step>
    </Steps>

    ### Interpret status and recover

    | Status      | Meaning                                                                                                       |
    | ----------- | ------------------------------------------------------------------------------------------------------------- |
    | `PENDING`   | At least one operation is still processing. Status queries can return this; `waitForExecution` keeps polling. |
    | `COMPLETED` | Every operation completed successfully. Completed operations include `txHash`.                                |
    | `FAILED`    | At least one operation failed. Inspect its `failureReason` and the execution error context.                   |

    Re-quote and collect fresh signatures after quote expiry. For `REVERTED`, fix the target calls or stale swap assumptions before requesting another signature. For `BRIDGE_REFUNDED`, inspect the error context for known refunds; a refund does not turn the intent into a success.

    If submission or polling returns an uncertain network result, query the existing intent before creating another. Keep the quote or submission `traceId` for support.
  </Tab>

  <Tab title="REST API">
    A direct integration uses `POST /quotes`, `POST /intents`, and `GET /intents/{id}`. This EOA example delivers 100 USDC from Base to the same EOA on Arbitrum, without destination calls. Unlike the other tabs, it does not forward the tokens to a payment recipient.

    Keep the API key on your backend. The signing example assumes `walletClientFor(chainId)` returns a viem wallet client for the same EOA on each requested chain.

    <Steps>
      <Step title="Request and select a route">
        ```ts {14,18-21} theme={null}
        const baseUrl = "https://v1.orchestrator.rhinestone.dev";
        const [eoaAddress] = await walletClientFor(base.id).getAddresses();
        if (!eoaAddress) throw new Error("Connect an EOA first");

        const apiKey = process.env.RHINESTONE_API_KEY;
        if (!apiKey) throw new Error("RHINESTONE_API_KEY is required");

        const headers = {
          "Content-Type": "application/json",
          "x-api-key": apiKey,
          "x-api-version": "2026-04.blanc",
        };

        const quoteResponse = await fetch(`${baseUrl}/quotes`, {
          method: "POST",
          headers,
          body: JSON.stringify({
            account: { address: eoaAddress, accountType: "EOA" },
            destinationChainId: `eip155:${arbitrum.id}`,
            tokenRequests: [
              { tokenAddress: usdcOnArbitrum, amount: amount.toString() },
            ],
            accountAccessList: {
              chainTokens: {
                [`eip155:${base.id}`]: [usdcOnBase],
              },
            },
          }),
        });

        if (!quoteResponse.ok) throw new Error(await quoteResponse.text());
        const { routes } = await quoteResponse.json();
        const route = routes[0];
        if (!route) throw new Error("No route available");
        ```

        Inspect the selected route's `cost`, `expiresAt`, and settlement layer before asking for authorization. See [Create Quote](https://docs.rhinestone.dev/api-reference/orchestrator/quotes/create-quote) for the complete request and response.
      </Step>

      <Step title="Complete token requirements" id="complete-token-requirements">
        For an EOA, inspect the selected route's `tokenRequirements`. For every requirement on its keyed source chain:

        * For `approval`, call `approve(requirement.spender, requirement.amount)` on the keyed ERC-20 token.
        * For `wrap`, deposit at least `requirement.amount` into that chain's wrapped-native token contract.
        * Wait for each transaction receipt before continuing. Re-quote if the route expires while the user completes these transactions.

        This is based on account type, not client choice. An EOA using REST or `@rhinestone/sdk` must complete the same operations. For the SDK's recommended route, read `prepared.quotes.best.tokenRequirements`; the SDK does not execute them.

        For a smart account, set `account.accountType` to `ERC7579` and include required `setupOps` for an undeployed account. Approvals and wrapping run as preclaim operations instead of EOA token requirements. Embedded wallets use this smart-account path.
      </Step>

      <Step title="Sign the selected route">
        Sign every origin payload in order and the destination payload when present:

        ```ts theme={null}
        import type { TypedDataDefinition } from "viem";

        async function signEvmPayload(
          payload: TypedDataDefinition & { kind?: string },
        ) {
          if (payload.kind && payload.kind !== "eip712") {
            throw new Error(`Unsupported payload: ${payload.kind}`);
          }

          const { kind: _kind, ...typedData } = payload;
          const chainId = Number(typedData.domain?.chainId);
          if (!Number.isSafeInteger(chainId) || chainId <= 0) {
            throw new Error("Signing payload has no valid EVM chain ID");
          }

          return walletClientFor(chainId).signTypedData({
            ...typedData,
            account: eoaAddress,
          });
        }

        if (Date.now() >= route.expiresAt * 1_000) {
          throw new Error("Quote expired; request and review a new route");
        }

        const origin = await Promise.all(route.signData.origin.map(signEvmPayload));
        const destination = route.signData.destination
          ? await signEvmPayload(route.signData.destination)
          : undefined;
        ```

        These signatures authorize only this route. A raw smart-account integration must encode signatures for its ERC-1271 validator and sign `targetExecution` when present. Use the custom signer SDK or an embedded wallet unless your integration implements that account-specific encoding.
      </Step>

      <Step title="Submit the selected route">
        ```ts theme={null}
        if (Date.now() >= route.expiresAt * 1_000) {
          throw new Error("Quote expired; request and sign a new route");
        }

        const submitResponse = await fetch(`${baseUrl}/intents`, {
          method: "POST",
          headers,
          body: JSON.stringify({
            intentId: route.intentId,
            signatures: {
              origin,
              ...(destination ? { destination } : {}),
            },
          }),
        });

        if (!submitResponse.ok) throw new Error(await submitResponse.text());
        const result = await submitResponse.json();
        ```

        Keep `route.intentId` even if the submission response is lost. Query it before retrying so you do not create a duplicate intent. See [Create Intent](https://docs.rhinestone.dev/api-reference/orchestrator/intents/create-intent) for the full submission contract.
      </Step>

      <Step title="Track execution">
        ```ts theme={null}
        async function pollIntent(intentId: string) {
          for (let attempt = 0; attempt < 60; attempt++) {
            const response = await fetch(`${baseUrl}/intents/${intentId}`, { headers });
            if (!response.ok) throw new Error(await response.text());

            const latest = await response.json();
            if (latest.status === "COMPLETED" || latest.status === "FAILED") {
              return latest;
            }

            await new Promise((resolve) => setTimeout(resolve, 2_000));
          }

          return undefined;
        }

        const latest = await pollIntent(result.intentId);
        console.log(result.intentId, latest);
        ```

        `PENDING` is non-terminal. An exhausted poll or failed status request leaves the outcome unknown; keep the ID and query again later. On `FAILED`, inspect `operations` for the failed chain, operation, and `failureReason`. See [Get Intent](https://docs.rhinestone.dev/api-reference/orchestrator/intents/get-intent) for the complete status response.
      </Step>
    </Steps>
  </Tab>
</Tabs>

<CardGroup cols={2}>
  <Card title="Send a transaction" icon="send" href="/transactions/multichain/send-a-transaction">
    Build token requests, calls, and source constraints.
  </Card>

  <Card title="Unified balance" icon="wallet" href="/transactions/multichain/unified-balance">
    Read spendable balances across supported chains.
  </Card>
</CardGroup>
