Skip to main content

Guide: Native Swap Example

This guide provides a complete, code-first walkthrough for creating and submitting a swap transaction using the API. We will build a real-world example: swapping USD to cBTC on the Citrea blockchain backed by Sepolia ETH as collateral, using Crest as a liquidity source.

The Goal

Our goal is to create an “intent” that performs the following actions:
  1. On Sepolia Testnet: A user deposits 0.0001 Sepolia ETH into an escrow EOA.
  2. On Citrea Testnet: Using the value from the deposit, execute a swap for cBTC via crest contract.
  3. Finally: The solver sends the principal amount of the deposit back to the user on Citrea.
This entire process will be defined in a single intent, signed by the user once, and submitted to the API.

Prerequisites

You’ll need viem to interact with chains, create authorizations, and sign messages. For frontend applications, you would need to use privy SDK to work with embedded wallets.
import {
  createWalletClient,
  http,
  parseUnits,
  getAddress,
  formatEther
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { citreaTestnet, sepolia } from "viem/chains";

Step 1: Setting up Accounts and Clients

First, we define the accounts and blockchain clients we’ll be working with. In a real application, the user would be the end-user interacting with your application, and their private key would be managed by their wallet.
// A sample user account
const user = privateKeyToAccount("0x...");

// The escrow contract address where the initial deposit is sent
const escrowAddress = getAddress("0x...");


const publicSepoliaClient = createPublicClient({
  chain: sepolia,
  transport: http(),
});

const publicCitreaClient = createPublicClient({
  chain: citreaTestnet,
  transport: http(),
});

Step 2: Define the Intent Parameters

We’ll define the amounts and generate a quote for the swap part of our intent.
const depositAmount = parseUnits("0.0001", 18);

// In a real app, you would call an external service like crest's /rfqt endpoint
// to get a signed quote for the swap.
const crestRfqt = await generateRfqt({
  quoteId: "0x...",
  user: user.address,
  tokenIn: "usd",
  tokenOut: "bitcoin",
  amountIn: formatEther(depositAmount),
  amountOut: "100", // Targeting 100 USD
  expiry: Math.floor(Date.now() / 1000) + 60, // 1 minute expiry
});

Step 3: Create EIP-7702 Authorizations

The core of the process is the user granting our system one-time authority on each chain. The user’s wallet will sign an EIP-7702 authorization for each delegate contract on each chain.
// Fetch the current nonce for the user on each chain
const nonceSepolia = await publicSepoliaClient.getTransactionCount({address: user.address});
const nonceCitrea = await publicCitreaClient.getTransactionCount({address: user.address});

const delegateAddressSepolia = getAddress("0x36d5e6b93c211b79b734b0adb3812d0c066608f9");
const delegateAddressCitrea = getAddress("0x446448759096065EE9c173452024D5F9660Ec816");

// User signs an authorization for Sepolia
const userAuthSepolia = await user.signAuthorization({
  address: delegateAddressSepolia,
  chainId: sepolia.id,
  nonce: nonceSepolia,
});

// User signs another authorization for Citrea
const userAuthCitrea = await user.signAuthorization({
  address: delegateAddressCitrea,
  chainId: citreaTestnet.id,
  nonce: nonceCitrea,
});
These authorization objects are what a solver will use to execute transactions on the user’s behalf.

Step 4: Construct the Chain Batches

Now we define the specific on-chain actions. Each object in the chainBatches array represents a set of calls to be executed on a specific blockchain, in order.
import { hashChainBatches, getIntentHash } from "../src/authorization";

const recentBlockSepolia = await publicSepoliaClient.getBlockNumber();
const recentBlockCitrea = await publicCitreaClient.getBlockNumber();

const chainBatches = hashChainBatches([
  // Step 0: Deposit Sepolia ETH to escrow
  {
    chainId: sepolia.id,
    recentBlock: recentBlockSepolia,
    calls: [
      {
        to: escrowAddress,
        data: "0x",
        value: depositAmount,
      },
    ],
  },
  // Step 1: Swap for cBTC on Citrea
  {
    chainId: citreaTestnet.id,
    // We add a buffer of 8 blocks to ensure the Sepolia transaction has time to confirm
    // before this step becomes executable.
    recentBlock: recentBlockCitrea + 8n,
    calls: [
      // The RFQT object from Step 2 is a valid "Call" object
      crestRfqt,
    ],
  },
]);
The hashChainBatches utility processes this structure to create the hashes that will be signed.

Step 5: Sign the Intent and Prepare the Request

Finally, the user signs the intent itself. The signature is over the computed hash of the chainBatches. This proves the user agrees to this specific sequence of actions.
// Create the digest that the user will sign
const digest = getIntentHash(chainBatches);

// User signs the message
const signature = await user.signMessage({
  message: {
    raw: digest,
  },
});

// Assemble the final request body for the API
const apiRequest = {
  address: user.address,
  authorization: [userAuthSepolia, userAuthCitrea],
  intentAuthorization: {
    signature,
    chainBatches,
  },
  tokenAddress: "0x0", // Native token
  tokenAmount: depositAmount,
};
This apiRequest object is what you send to the POST /transaction/submit endpoint.
curl -X POST https://tx-api.spicenet.io/transaction/submit \
  -H "Content-Type: application/json" \
  -d '{
    "address": "0x...",
    "authorization": [ { ... }, { ... } ],
    "intentAuthorization": {
      "signature": "0x...",
      "chainBatches": [ { ... }, { ... } ]
    },
    "tokenAddress": "0x0",
    "tokenAmount": 100000000000000
  }'

What Happens Next: The Solver

After you submit the intent, the API makes it available to a network of solvers. A solver will:
  1. Pick up your intent.
  2. Execute Step 0 by sending a transaction to the Delegate contract on Sepolia, using your signed authorization. This deposits your funds to escrow.
  3. Wait for the required block confirmations.
  4. Execute Step 1 by sending a transaction to the Delegate contract on Citrea, which performs the swap.
  5. As part of the final transaction, the solver also transfers the principal deposit amount (tokenAmount) back to your address (address) on the destination chain (Citrea).
Your application can track the status of the intent using the GET /intent/{intentId}/step/{stepId}/status endpoint. You have successfully orchestrated a cross-chain action without requiring the user to switch networks or manage gas on the destination chain.