> ## 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.

# Quickstart

> Create a smart account and send your first crosschain transaction with the Rhinestone SDK.

export const VmCodeGroup = ({children, caption, label = "Virtual machine"}) => {
  const [vm, setVm] = useState("evm");
  const [open, setOpen] = useState(false);
  const selectorRef = useRef(null);
  const triggerRef = useRef(null);
  const menuRef = useRef(null);
  const focusIndex = useRef(0);
  const options = [{
    value: "evm",
    title: "EVM",
    icon: "/images/networks/ethereum.svg"
  }, {
    value: "solana",
    title: "Solana",
    icon: "/images/networks/solana.svg"
  }];
  if (typeof caption !== "string" || caption.trim() === "") {
    throw new Error("VmCodeGroup requires a non-empty caption.");
  }
  const selected = options.find(option => option.value === vm);
  const blocks = (Array.isArray(children) ? children : [children]).filter(child => typeof child !== "string" || child.trim() !== "");
  if (blocks.length !== 2 || !blocks.every(block => typeof block?.props?.language === "string" && block.props.numberOfLines !== undefined) || !options.every(option => blocks.filter(block => block.props.filename === option.title).length === 1)) {
    throw new Error("VmCodeGroup requires two fenced code blocks titled EVM and Solana, with no other content.");
  }
  useEffect(() => {
    const path = window.location.pathname;
    const selection = window.__rhinestoneDocsVm;
    if (selection?.path === path) setVm(selection.vm);
    const onChange = event => {
      if (event.detail.path === path) {
        setVm(event.detail.vm);
        setOpen(false);
      }
    };
    window.addEventListener("rhinestone-docs-vm", onChange);
    return () => window.removeEventListener("rhinestone-docs-vm", onChange);
  }, []);
  useEffect(() => {
    if (!open) return;
    menuRef.current?.querySelectorAll("button")[focusIndex.current]?.focus();
    const dismiss = event => {
      if (!selectorRef.current?.contains(event.target)) setOpen(false);
    };
    document.addEventListener("pointerdown", dismiss);
    return () => document.removeEventListener("pointerdown", dismiss);
  }, [open]);
  const selectVm = value => {
    const selection = {
      path: window.location.pathname,
      vm: value
    };
    window.__rhinestoneDocsVm = selection;
    window.dispatchEvent(new CustomEvent("rhinestone-docs-vm", {
      detail: selection
    }));
    triggerRef.current?.focus();
  };
  const openMenu = index => {
    focusIndex.current = index;
    setOpen(true);
    menuRef.current?.querySelectorAll("button")[index]?.focus();
  };
  const handleMenuKey = event => {
    const buttons = Array.from(menuRef.current.querySelectorAll("button"));
    const current = buttons.indexOf(document.activeElement);
    let next;
    if (event.key === "ArrowDown") next = (current + 1) % buttons.length;
    if (event.key === "ArrowUp") next = (current + buttons.length - 1) % buttons.length;
    if (event.key === "Home" || event.key.toLowerCase() === "e") next = 0;
    if (event.key === "End" || event.key.toLowerCase() === "s") next = buttons.length - 1;
    if (next !== undefined) {
      event.preventDefault();
      buttons[next].focus();
    }
    if (event.key === "Tab") setOpen(false);
  };
  return <div className="vm-code-group">
      <div className="vm-code-group-header">
        <span className="vm-code-caption" title={caption}>
          {caption}
        </span>
        <div className="vm-code-selector" ref={selectorRef} onBlur={event => {
    if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
  }} onKeyDown={event => {
    if (event.key === "Escape" && open) {
      event.preventDefault();
      setOpen(false);
      triggerRef.current?.focus();
    }
  }}>
          <button type="button" className="vm-code-trigger" ref={triggerRef} aria-label={`${label}: ${selected.title}`} aria-haspopup="menu" aria-expanded={open} onClick={() => open ? setOpen(false) : openMenu(options.indexOf(selected))} onKeyDown={event => {
    if (event.key === "ArrowDown" || event.key === "ArrowUp") {
      event.preventDefault();
      openMenu(event.key === "ArrowDown" ? 0 : options.length - 1);
    }
  }}>
            <img className="vm-code-icon" src={selected.icon} alt="" width="16" height="16" />
            <span>{selected.title}</span>
            <svg className="vm-code-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <path d="m7 10 5 5 5-5" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </button>
          {open && <div className="vm-code-menu" role="menu" aria-label={label} ref={menuRef} onKeyDown={handleMenuKey}>
              {options.map(option => <button type="button" role="menuitemradio" aria-checked={vm === option.value} tabIndex={-1} className="vm-code-option" key={option.value} onClick={() => selectVm(option.value)}>
                  <img className="vm-code-icon" src={option.icon} alt="" width="16" height="16" />
                  <span>{option.title}</span>
                  {vm === option.value && <svg className="vm-code-check" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                      <path d="m5 12 4 4L19 6" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
                    </svg>}
                </button>)}
            </div>}
        </div>
      </div>
      {options.map(option => {
    const block = blocks.find(block => block.props.filename === option.title);
    const CodeBlock = block.type;
    return <div key={option.value} className="vm-code-group-panel" data-vm={option.value} hidden={option.value !== vm}>
            <CodeBlock {...block.props} filename={undefined} />
          </div>;
  })}
    </div>;
};

Create an EVM account for a crosschain transfer, or a Solana account for an SPL transfer. Select the same VM throughout; paste the shared blocks and selected examples into `quickstart.ts` in order.

## Prerequisites

Run this example in a secure server-side environment. You need:

* A `RHINESTONE_API_KEY` from the [Rhinestone dashboard](https://dashboard.rhinestone.dev)
* For EVM, a funding account with Base Sepolia ETH and its private key in `FUNDING_PRIVATE_KEY`
* For Solana, configured deployment sponsorship and devnet USDC to fund the wallet after deployment

Get Base Sepolia ETH from the [QuickNode faucet](https://faucet.quicknode.com/drip) or [Alchemy faucet](https://www.alchemy.com/faucets). Keep private keys and the API key private. Solana network and route availability depend on your environment; SDK support does not guarantee a live route. See [Solana support](/wallets/custom-signer/solana-support).

<Note>
  This guide targets SDK 3.0.0, which is not released yet. The install commands
  below are for use after publication.
</Note>

Install the SDK and its compatible viem peer:

<CodeGroup>
  ```bash npm theme={null}
  npm install viem@^2.55.0 @rhinestone/sdk@3.0.0 && npm install --save-dev tsx @types/node
  ```

  ```bash pnpm theme={null}
  pnpm add viem@^2.55.0 @rhinestone/sdk@3.0.0 && pnpm add --save-dev tsx @types/node
  ```

  ```bash bun theme={null}
  bun add viem@^2.55.0 @rhinestone/sdk@3.0.0 && bun add --dev tsx @types/node
  ```
</CodeGroup>

Initialize the SDK once for either VM:

```ts theme={null}
import { RhinestoneSDK } from '@rhinestone/sdk'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'

const apiKey = process.env.RHINESTONE_API_KEY
if (!apiKey) throw new Error('RHINESTONE_API_KEY is required')
const ownerPrivateKey = generatePrivateKey()
const owner = privateKeyToAccount(ownerPrivateKey)
const rhinestone = new RhinestoneSDK({
  auth: { mode: 'apiKey', apiKey },
})
```

Store `ownerPrivateKey` securely before funding either account; this generated example key is not recoverable after the script exits.

<Steps>
  <Step title="Create an account">
    Create an account controlled by the owner above. This computes its address without deploying it.

    <VmCodeGroup caption="Create your smart account">
      ```ts EVM theme={null}
      import {
        createPublicClient,
        createWalletClient,
        encodeFunctionData,
        erc20Abi,
        http,
        parseEther,
        parseUnits,
        zeroAddress,
        type Hex,
      } from 'viem'
      import { arbitrumSepolia, baseSepolia } from 'viem/chains'

      const fundingPrivateKey = process.env.FUNDING_PRIVATE_KEY
      if (!fundingPrivateKey) throw new Error('FUNDING_PRIVATE_KEY is required')
      const sourceChain = baseSepolia
      const targetChain = arbitrumSepolia
      // This owner determines the counterfactual address.
      // Deployment happens on first use on each chain.
      const account = await rhinestone.createAccount({
        evm: {
          owners: {
            type: 'ecdsa',
            accounts: [owner],
          },
        },
      })
      const address = account.getAddress('evm')

      console.log('Smart account address:', address)
      ```

      ```ts Solana theme={null}
      import { createSolanaSwigId, solanaDevnet } from '@rhinestone/sdk/solana'

      // Persist id and swig with the owner configuration before funding.
      const { id, swig } = createSolanaSwigId()
      const account = await rhinestone.createAccount({
        solana: { swig, owner: { type: 'ecdsa', account: owner } },
      })
      // Fund the wallet address, not the Swig state address.
      const address = account.getAddress('solana')
      console.log('Solana wallet address:', address)
      ```
    </VmCodeGroup>
  </Step>

  <Step title="Fund the account">
    Fund the account's wallet address with enough testnet assets for the transfer and quoted fees.

    <VmCodeGroup caption="Fund your account">
      ```ts EVM theme={null}
      const publicClient = createPublicClient({
        chain: sourceChain,
        transport: http(),
      })
      const fundingAccount = privateKeyToAccount(fundingPrivateKey as Hex)
      const fundingClient = createWalletClient({
        account: fundingAccount,
        chain: sourceChain,
        transport: http(),
      })

      // Fund with Base Sepolia ETH.
      const txHash = await fundingClient.sendTransaction({
        to: address,
        value: parseEther('0.001'),
      })
      await publicClient.waitForTransactionReceipt({ hash: txHash })
      ```

      ```ts Solana theme={null}
      // Creation is sponsored and installs a permanent root authority.
      // Deploy before funding; never prefund an undeployed wallet.
      await account.deploy('solana', solanaDevnet, { swigId: id })

      // true can also mean the Swig already exists, without checking its owner.
      // Verify the onchain root matches owner, then send devnet USDC to address.
      ```
    </VmCodeGroup>

    See [Account deployment](/transactions/relaying/account-deployment) for readiness checks before funding.
  </Step>

  <Step title="Prepare and send a transaction">
    Prepare a transfer of 0.1 USDC. Review the selected quote before signing.

    <VmCodeGroup caption="Prepare a transfer">
      ```ts EVM theme={null}
      const usdcAmount = parseUnits('0.1', 6)
      const arbitrumSepoliaUsdc = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d'
      const recipient = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045'

      // Source Base Sepolia ETH to transfer USDC on Arbitrum Sepolia.
      const prepared = await account.prepareTransaction({
        source: { chain: sourceChain, token: zeroAddress },
        destination: {
          chain: targetChain,
          token: arbitrumSepoliaUsdc,
          amount: usdcAmount,
          calls: [
            {
              to: arbitrumSepoliaUsdc,
              value: 0n,
              data: encodeFunctionData({
                abi: erc20Abi,
                functionName: 'transfer',
                args: [recipient, usdcAmount],
              }),
            },
          ],
        },
      })
      ```

      ```ts Solana theme={null}
      import { solanaAddress } from '@rhinestone/sdk/solana'

      // Set SOLANA_RECIPIENT to another wallet's base58 address.
      const recipientInput = process.env.SOLANA_RECIPIENT
      if (!recipientInput) throw new Error('SOLANA_RECIPIENT is required')
      const usdcMint = solanaAddress('4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU')
      // Same-cluster SPL transfer; the USDC balance must also cover fees.
      const prepared = await account.prepareTransaction({
        source: { chain: solanaDevnet, token: usdcMint },
        destination: {
          chain: solanaDevnet,
          token: usdcMint,
          amount: 100_000n,
          recipient: solanaAddress(recipientInput),
        },
      })
      ```
    </VmCodeGroup>

    Use the same signing and submission lifecycle for either VM:

    ```ts theme={null}
    const signed = await account.signTransaction(prepared)
    const submitted = await account.submitTransaction(signed)
    const result = await account.waitForExecution(submitted)

    console.log('Result:', result)
    ```

    `submitTransaction` returns an intent handle. `waitForExecution` waits for a terminal outcome: it resolves on success and throws on failure. Keep the submitted intent ID so you can inspect its status before retrying.
  </Step>
</Steps>

Run the completed script:

```bash theme={null}
npx tsx quickstart.ts
```

## Expected outcome

On successful execution, the result has the status `COMPLETED`. The EVM recipe deploys the account on Base Sepolia and Arbitrum Sepolia and transfers 0.1 USDC on Arbitrum Sepolia. The Solana recipe transfers 0.1 devnet USDC from the deployed wallet.

## Next steps

<CardGroup cols={3}>
  <Card title="Configure the account" icon="settings" href="/wallets/custom-signer/account-setup/create-an-account">
    Choose an owner and account implementation.
  </Card>

  <Card title="Send another transaction" icon="arrow-right" href="/transactions/multichain/send-a-transaction">
    Learn the complete multichain transaction flow.
  </Card>

  <Card title="Sponsor fees" icon="fuel" href="/transactions/sponsorship/set-up">
    Cover selected transaction costs for your users.
  </Card>
</CardGroup>


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