Prepare Tokens for Bridging

## Prepare Tokens for Bridging

To bridge ETH, you need to wrap it into WETH first. The WETH contract on Ethereum mainnet is:

```
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
```

!!!note "Why WETH instead of ETH?"
    Snowbridge only supports ERC-20 tokens—it cannot bridge native ETH directly. The bridge uses a standardized token interface to lock assets on Ethereum and mint corresponding representations on Polkadot. Since native ETH doesn't conform to the ERC-20 standard, you must first wrap it into WETH (Wrapped ETH), an ERC-20-compliant token pegged 1:1 to ETH.

You can wrap ETH by:

1. Visiting the [WETH contract on Etherscan](https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2#writeContract)
2. Connecting your wallet
3. Calling the `deposit` function with the amount of ETH you want to wrap


Transfer Assets from Ethereum into Polkadot

Initialize Your Project

## Initialize Your Project

Follow these steps to setup and initialize your project:
1. Create the project folder using the following commands:

```bash
mkdir eth-to-polkadot-bridge && \
cd eth-to-polkadot-bridge
```

2. Initialize the JavaScript project:

```bash
npm init -y && npm pkg set type=module
```

Install dev dependencies:

```bash
npm install --save-dev @types/node tsx typescript
```

Sending assets from Ethereum to Polkadot requires the PJS version of the ParaSpell SDK. Install the necessary dependencies using the following command:

```bash
npm install @paraspell/sdk-pjs@13.2.2 @polkadot/api@16.5.6 @polkadot/types@16.5.6 ethers@6.16.0
```

Now add the following setup code to `index.ts`:

```ts title="index.ts"
import { EvmBuilder, getTokenBalance, approveToken, getBridgeStatus } from '@paraspell/sdk-pjs';
import { ethers } from 'ethers';

// Ethereum mainnet RPC endpoint (use your own API key)
const ETH_RPC = 'https://eth-mainnet.g.alchemy.com/v2/INSERT_API_KEY';

// WETH contract address on Ethereum mainnet
const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';

// WETH has 18 decimals
const WETH_UNITS = 1_000_000_000_000_000_000n;

// Amount to bridge: 0.001 WETH (raw units as bigint)
const AMOUNT = WETH_UNITS / 1000n;

// Your Polkadot address to receive the bridged tokens (SS58 format)
const RECIPIENT_ADDRESS = 'INSERT_YOUR_POLKADOT_ADDRESS';

// Connect to Ethereum mainnet
async function getProviderAndSigner();
}
```

Replace `INSERT_YOUR_POLKADOT_ADDRESS` with your Polkadot account address (SS58 format) that will receive the bridged tokens on Polkadot Hub.

!!!warning "Security Warning"
    Never commit private keys or seed phrases to production code. Use environment variables or secure key management systems.


Approve Tokens for Bridging

## Approve Tokens for Bridging

Before bridging ERC-20 tokens, you must approve the Snowbridge Gateway contract to spend your tokens. The ParaSpell SDK provides the following helper functions for approving spending:

```ts title="index.ts"
async function approveTokens() = await getProviderAndSigner();

  // Check current WETH balance
  const balance = await getTokenBalance(signer, 'WETH');
  console.log(`Current WETH balance: ${balance}`);

  if (BigInt(balance) === 0n)`);
    process.exit(1);
  }

  // Approve the Snowbridge Gateway contract to spend WETH
  const { result: approveTx } = await approveToken(signer, AMOUNT, 'WETH');
  console.log(`Approval transaction hash: ${approveTx.hash}`);

  // Wait for confirmation
  await approveTx.wait();
  console.log('Token approval confirmed!');
}
```

Run the approval script:

```bash
npx tsx index.ts
```

You will see output confirming the token balance and approval similar to the following:

  npx tsx index.ts
  Current WETH balance: 0.05
  Approval transaction hash: 0x1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890
  Token approval confirmed!


Build and Execute the Bridge Transfer

## Build and Execute the Bridge Transfer

Now build and execute the bridge transfer from Ethereum to Polkadot Hub:

```ts title="index.ts"
async function bridgeToPolkadot() = await getProviderAndSigner();

  console.log('Building bridge transaction...');
  console.log(`Bridging ${AMOUNT} WETH to Polkadot Hub`);
  console.log(`Recipient: ${RECIPIENT_ADDRESS}`);

  // Build and execute the bridge transfer
  // Note: 'AssetHubPolkadot' is the SDK identifier for Polkadot Hub
  const txHash = await EvmBuilder(provider)
    .from('Ethereum')
    .to('AssetHubPolkadot')
    .currency({
      symbol: 'WETH',
      amount: AMOUNT,
    })
    .recipient(RECIPIENT_ADDRESS)
    .signer(signer)
    .build();

  console.log('Bridge transaction submitted!');
  console.log(`Transaction hash: ${txHash}`);
  console.log('Transfer will arrive in approximately 30 minutes.');

  // Wait for Ethereum confirmation
  const receipt = await provider.waitForTransaction(txHash);
  console.log(
```

Comment out the `approveTokens()` function and run the transfer:

```bash
npx tsx index.ts
```

After submitting the transaction, you will see the Ethereum transaction hash included in the terminal output similar to the following:

  npx tsx index.ts
  Building bridge transaction...
  Bridging 1000000000000000 WETH to Polkadot Hub
  Recipient: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
  Bridge transaction submitted!
  Transaction hash: 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
  Transfer will arrive in approximately 30 minutes.
  Ethereum transaction confirmed! Waiting for bridge relay...


Monitor the Transfer

## Monitor the Transfer

Bridge transfers from Ethereum to Polkadot take approximately 30 minutes. You can monitor the transfer status in several ways:

- **Snowbridge App** - Visit [app.snowbridge.network](https://app.snowbridge.network/) to check the transfer history
- **Etherscan** - Track your Ethereum transaction on [etherscan.io](https://etherscan.io/)
- **Polkadot.js Apps** - Monitor incoming transfers on [Polkadot Hub](https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fpolkadot-asset-hub-rpc.polkadot.io#/accounts)

You can also check the bridge status programmatically:

```ts title="index.ts"

bridgeToPolkadot();

async function checkBridgeStatus() from '@paraspell/sdk-pjs';

// Get assets supported for Ethereum → Polkadot Hub transfers
const supportedAssets = getSupportedAssets('Ethereum', 'AssetHubPolkadot');
console.log('Supported assets:', supportedAssets.map(a => a.symbol));
```

Running this script will return a list of supported assets in the terminal similar to the following:

  npx tsx get-supported-assets.ts
  Supported assets: [
    'WETH',    'WBTC',  'USDC',  'USDT',
    'DAI',     'SHIB',  'LINK',  'AAVE',
    'wstETH',  'tBTC',  'PEPE',  'EURC',
    'MYTH',    'TRAC',  'KILT',  'DOT',
    ... and more
  ]

Common supported tokens on Polkadot Hub include:

| Token | Symbol | Description |
|-------|--------|-------------|
| Wrapped Ether | WETH | Ethereum's native token wrapped as ERC-20 |
| Wrapped Bitcoin | WBTC | Bitcoin wrapped as an ERC-20 token |
| Shiba Inu | SHIB | Popular ERC-20 meme token |

To transfer to a different destination, change the `.to()` parameter and verify the asset is supported using the following code:

```ts title="index.ts"
// Check if WETH is supported on Hydration
const hydrationAssets = getSupportedAssets('Ethereum', 'Hydration');
const wethSupported = hydrationAssets.some(a => a.symbol === 'WETH');

if (wethSupported))
    .recipient(recipientAddress)
    .signer(signer)
    .build();
}
```

  npx tsx bridge-to-hydration.ts
  WETH supported on Hydration: true
  Building bridge transaction to Hydration...
  Bridge transaction submitted!
  Transaction hash: 0x1234...abcd

???- code "Complete `index.ts` script"

    ```typescript title="index.ts"
    import { EvmBuilder, getTokenBalance, approveToken, getBridgeStatus } from '@paraspell/sdk-pjs';
    import { ethers } from 'ethers';

    // Ethereum mainnet RPC endpoint (use your own API key)
    const ETH_RPC = 'https://eth-mainnet.g.alchemy.com/v2/INSERT_API_KEY';

    // WETH contract address on Ethereum mainnet
    const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';

    // WETH has 18 decimals
    const WETH_UNITS = 1_000_000_000_000_000_000n;

    // Amount to bridge: 0.001 WETH (raw units as bigint)
    const AMOUNT = WETH_UNITS / 1000n;

    // Your Polkadot address to receive the bridged tokens (SS58 format)
    const RECIPIENT_ADDRESS = 'INSERT_YOUR_POLKADOT_ADDRESS';

    // Connect to Ethereum mainnet
    async function getProviderAndSigner();
    }

    async function approveTokens() = await getProviderAndSigner();

      // Check current WETH balance
      const balance = await getTokenBalance(signer, 'WETH');
      console.log(`Current WETH balance: ${balance}`);

      if (BigInt(balance) === 0n)`);
        process.exit(1);
      }

      // Approve the Snowbridge Gateway contract to spend WETH
      const { result: approveTx } = await approveToken(signer, AMOUNT, 'WETH');
      console.log(`Approval transaction hash: ${approveTx.hash}`);

      // Wait for confirmation
      await approveTx.wait();
      console.log('Token approval confirmed!');
    }

    approveTokens();

    async function bridgeToPolkadot() = await getProviderAndSigner();

      console.log('Building bridge transaction...');
      console.log(`Bridging ${AMOUNT} WETH to Polkadot Hub`);
      console.log(`Recipient: ${RECIPIENT_ADDRESS}`);

      // Build and execute the bridge transfer
      // Note: 'AssetHubPolkadot' is the SDK identifier for Polkadot Hub
      const txHash = await EvmBuilder(provider)
        .from('Ethereum')
        .to('AssetHubPolkadot')
        .currency({
          symbol: 'WETH',
          amount: AMOUNT,
        })
        .recipient(RECIPIENT_ADDRESS)
        .signer(signer)
        .build();

      console.log('Bridge transaction submitted!');
      console.log(`Transaction hash: ${txHash}`);
      console.log('Transfer will arrive in approximately 30 minutes.');

      // Wait for Ethereum confirmation
      const receipt = await provider.waitForTransaction(txHash);
      console.log(
        `Ethereum transaction confirmed in block ${receipt?.blockNumber}. Waiting for bridge relay...`,
      );
    }

    bridgeToPolkadot();

    async function checkBridgeStatus()

    checkBridgeStatus();
    ```

!!!info "Transfer Assets Out of Polkadot"

    To transfer assets out of Polkadot, you can use the ParaSpell XCM SDK to build and execute a bridge transfer from Polkadot to Ethereum. Check out the [Polkadot to Ethereum](https://paraspell.github.io/docs/xcm-sdk/send-xcm.html#polkadot-ethereum-transfer) section of the ParaSpell XCM SDK documentation.


Where to Go Next

## Where to Go Next

- **External Documentation** - Dive deeper into the [ParaSpell XCM SDK](https://paraspell.github.io/docs/xcm-sdk/getting-started.html) and [Snowbridge](https://docs.snowbridge.network/) documentation resources.
- **Learn about XCM** - Understand the underlying protocol by visiting the [Get Started with XCM](/parachains/interoperability/get-started/) guide


Introduction

## Introduction

This guide walks you through transferring tokens between two parachains using the [ParaSpell XCM SDK](/reference/tools/paraspell/). This example utilizes [Asset Hub](/reference/polkadot-hub/#asset-management) and the [People Chain](/reference/polkadot-hub/#people-chain). However, the same approach can be applied to transfers between other parachains.

For development purposes, this guide will use the [Polkadot TestNet](/smart-contracts/connect/#networks-details), so the transferred token will be PAS.

In this guide, you will:

- Build an XCM transfer transaction using ParaSpell XCM SDK.
- Perform a dry run to validate the transfer.
- Verify the Existential Deposit (ED) requirement on the destination chain.
- Retrieve information regarding the transfer, along with fee estimates.
- Submit the transaction.


Prerequisites

### Prerequisites

Before you begin, ensure you have the following:

- Knowledge of the [fundamentals of Polkadot](/parachains/get-started/).
- Basic understanding of [XCM](/parachains/interoperability/get-started/).
- Basic familiarity with JavaScript or TypeScript.
- Installed [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), a JavaScript and TypeScript package manager.


Initialize Your Project

## Initialize Your Project

Create the project folder:

```bash
mkdir paraspell-transfer && \
cd paraspell-transfer
```

Initialize the JavaScript project:

```bash
npm init -y
```

Install dev dependencies:

```bash
npm install --save-dev @types/node@^22.12.0 tsx@^4.20.6 typescript@^5.7.3
```

Install the required dependencies:

```bash
npm install --save @paraspell/sdk@13.2.2 polkadot-api@2.0.1 @polkadot-labs/hdkd-helpers@0.0.29 @polkadot-labs/hdkd@0.0.28
```

Now add the following setup code to `index.ts`:

```ts title="index.ts"
import { Builder, hasDryRunSupport } from '@paraspell/sdk';
import {
  entropyToMiniSecret,
  mnemonicToEntropy,
  ss58Address,
} from '@polkadot-labs/hdkd-helpers';
import { getPolkadotSigner } from 'polkadot-api/signer';
import { sr25519CreateDerive } from '@polkadot-labs/hdkd';
import { inspect } from 'util';

// PAS token has 10 decimals
const PAS_UNITS = 10_000_000_000n;

const SEED_PHRASE =
  'INSERT_YOUR_SEED_PHRASE';

// Create Sr25519 signer from mnemonic
function getSigner()

const RECIPIENT_ADDRESS = ss58Address(getSigner().publicKey);
const SENDER_ADDRESS = ss58Address(getSigner().publicKey);
```

Replace the `INSERT_YOUR_SEED_PHRASE` with the seed phrase from your Polkadot development account.

Be sure to fund this account with some PAS tokens on the Paseo Asset Hub using the [Polkadot Faucet](https://faucet.polkadot.io/?parachain=1000).

!!!warning "Security Warning"
    Never commit your mnemonic phrase to production code. Use environment variables or secure key management systems.


Build a Token Transfer Transaction

## Build a Token Transfer Transaction

The next step is to build the transaction that you intend to execute.

In this example, you will transfer 10 PAS tokens from Paseo's Asset Hub to Paseo's People Chain system parachain.

Add the ParaSpell transaction code to your `index.ts` file:

```ts title="index.ts"
async function transfer())
    .recipient(RECIPIENT_ADDRESS)
    .sender(SENDER_ADDRESS)
    .build();

  console.log('Built transaction:', inspect(tx, { colors: true, depth: null }));

  const result = await tx.signAndSubmit(signer);
  console.log(inspect(result, { colors: true, depth: null }));
  process.exit(0);
```

Do not execute it just yet. You will perform a dry run of this transaction first to ensure it works as expected.


Perform a Dry Run

## Perform a Dry Run

Dry runs simulate the transaction without broadcasting it, allowing you to confirm success in advance.

Add the following dry run code to your `index.ts` script:

```ts title="index.ts"

async function dryRunTransfer()

  const tx = await Builder()
    .from('AssetHubPaseo')
    .to('PeoplePaseo')
    .currency({
      symbol: 'PAS',
      amount: 10n * PAS_UNITS,
    })
    .recipient(RECIPIENT_ADDRESS)
    .sender(SENDER_ADDRESS)
    .dryRun();

  console.log(inspect(tx, { colors: true, depth: null }));
  process.exit(0);
}
```
Run the script using the following command:

```bash
npx tsx index.ts
```

The result of the dry run will look similar to the following example output:

  npx tsx index.ts
  {
  failureReason: undefined,
  failureChain: undefined,
  origin: {
    success: true,
    fee: 17965000n,
    currency: 'PAS',
    asset: {
      symbol: 'PAS',
      isNative: true,
      decimals: 10,
      existentialDeposit: '100000000',
      location: { parents: 1, interior: { Here: null } },
      isFeeAsset: true,
      amount: 100000000000n
    },
    weight: undefined,
    forwardedXcms: [
      {
        type: 'V3',
        value: {
          parents: 1,
          interior: { type: 'X1', value: { type: 'Parachain', value: 1004 } }
        }
      },
      [
        {
          type: 'V3',
          value: [
            {
              type: 'ReceiveTeleportedAsset',
              value: [
                {
                  id: {
                    type: 'Concrete',
                    value: {
                      parents: 1,
                      interior: { type: 'Here', value: undefined }
                    }
                  },
                  fun: { type: 'Fungible', value: 100000000000n }
                }
              ]
            },
            { type: 'ClearOrigin', value: undefined },
            {
              type: 'BuyExecution',
              value: {
                fees: {
                  id: {
                    type: 'Concrete',
                    value: {
                      parents: 1,
                      interior: { type: 'Here', value: undefined }
                    }
                  },
                  fun: { type: 'Fungible', value: 100000000000n }
                },
                weight_limit: { type: 'Unlimited', value: undefined }
              }
            },
            {
              type: 'DepositAsset',
              value: {
                assets: {
                  type: 'Wild',
                  value: { type: 'AllCounted', value: 1 }
                },
                beneficiary: {
                  parents: 0,
                  interior: {
                    type: 'X1',
                    value: {
                      type: 'AccountId32',
                      value: {
                        network: undefined,
                        id: FixedSizeBinary {
                          asText: [Function (anonymous)],
                          asHex: [Function (anonymous)],
                          asOpaqueHex: [Function (anonymous)],
                          asBytes: [Function (anonymous)],
                          asOpaqueBytes: [Function (anonymous)]
                        }
                      }
                    }
                  }
                }
              }
            },
            {
              type: 'SetTopic',
              value: FixedSizeBinary {
                asText: [Function (anonymous)],
                asHex: [Function (anonymous)],
                asOpaqueHex: [Function (anonymous)],
                asBytes: [Function (anonymous)],
                asOpaqueBytes: [Function (anonymous)]
              }
            }
          ]
        }
      ]
    ],
    destParaId: 1004
  },
  assetHub: undefined,
  bridgeHub: undefined,
  destination: {
    success: true,
    fee: 817598n,
    currency: 'PAS',
    asset: {
      symbol: 'PAS',
      isNative: true,
      decimals: 10,
      existentialDeposit: '1000000000',
      location: { parents: 1, interior: { Here: null } },
      isFeeAsset: true
    },
    weight: { refTime: 176858000n, proofSize: 0n },
    forwardedXcms: [],
    destParaId: undefined
  },
  hops: []
}


Verify the Existential Deposit

## Verify the Existential Deposit

Check if the recipient account meets the Existential Deposit (ED) requirement before sending by using [`verifyEdOnDestination`](https://paraspell.github.io/docs/xcm-api/xcm-sdk-functionality.html#verify-ed-on-destination):

```ts title="index.ts"

async function verifyED())
    .recipient(RECIPIENT_ADDRESS)
    .sender(SENDER_ADDRESS)
    .verifyEdOnDestination();

  console.log(`ED verification ${isValid ? 'successful' : 'failed'}.`);
  process.exit(0);
}
```
Comment out the `dryRunTransfer()` function so that it is not executed again. Then, execute the `verifyED()` by running the following command:

```bash
npx tsx index.ts
```

After that, you will get output confirming the ED which will look similar to the following:

  npx tsx index.ts
  ED verification successful.


Get Transfer Info and Fee Estimates

## Get Transfer Info and Fee Estimates

Before sending an XCM transaction, it is helpful to estimate the fees associated with executing and delivering the cross-chain message.

ParaSpell has a helpful function for this: [`getTransferInfo()`](https://paraspell.github.io/docs/xcm-sdk/xcm-utils.html#xcm-transfer-info). This function returns an estimate of the associated XCM fees, along with the account's balance before and after the fees are paid.

```ts title="index.ts"

async function XcmTransferInfo())
    .recipient(RECIPIENT_ADDRESS)
    .sender(SENDER_ADDRESS)
    .getTransferInfo();

  console.log('Transfer Info:', info);
  process.exit(0);
}
```

Comment out the `verifyED()` function so it doesn't execute again. Then, execute the `XcmTransferInfo()` function by running the following command:

```bash
npx tsx index.ts
```

You will see all the information for your transfer similar to the following example:

  npx tsx index.ts
  Transfer Info: {
  chain: {
    origin: "AssetHubPaseo",
    destination: "PeoplePaseo",
    ecosystem: "PAS",
  },
  origin: {
    selectedCurrency: {
      sufficient: true,
      balance: 9799197260420n,
      balanceAfter: 9699197260420n,
      currencySymbol: "PAS",
      asset: [Object ...],
      existentialDeposit: 100000000n,
    },
    xcmFee: {
      sufficient: true,
      fee: 17965000n,
      balance: 9799197260420n,
      balanceAfter: 9799179295420n,
      currencySymbol: "PAS",
      asset: [Object ...],
    },
  },
  assetHub: undefined,
  bridgeHub: undefined,
  hops: [],
  destination: {
    receivedCurrency: {
      sufficient: true,
      receivedAmount: 99999182402n,
      balance: 199998364804n,
      balanceAfter: 299997547206n,
      currencySymbol: "PAS",
      asset: [Object ...],
      existentialDeposit: 1000000000n,
    },
    xcmFee: {
      fee: 817598n,
      balance: 199998364804n,
      balanceAfter: 299997547206n,
      currencySymbol: "PAS",
      asset: [Object ...],
    },
  },
}

Now that you have:

- Completed a successful dry run of the transaction.
- Verified the existential deposit on the recipient account.
- Obtained an estimate of the associated XCM fees.

You can execute the transfer function by adding the following function call:

```typescript title="index.ts"

```

Comment out the `XcmTransferInfo()` function so it doesn't execute again. Then, execute the transfer by running the following command:

```bash
npx tsx index.ts
```

Your `transfer` function will submit the transaction, and you will get the following output:

  npx tsx index.ts
  ...
  Built transaction: {
  getPaymentInfo: [AsyncFunction: getPaymentInfo],
  getEstimatedFees: [AsyncFunction: getEstimatedFees],
  decodedCall: {
    type: 'PolkadotXcm',
    value: {
      type: 'limited_teleport_assets',
      value: {
        dest: {
          type: 'V5',
          value: {
            parents: 1,
            interior: { type: 'X1', value: { type: 'Parachain', value: 1004 } }
          }
        },
        beneficiary: {
          type: 'V5',
          value: {
            parents: 0,
            interior: {
              type: 'X1',
              value: {
                type: 'AccountId32',
                value: {
                  network: undefined,
                  id: FixedSizeBinary {
                    asText: [Function (anonymous)],
                    asHex: [Function (anonymous)],
                    asOpaqueHex: [Function (anonymous)],
                    asBytes: [Function (anonymous)],
                    asOpaqueBytes: [Function (anonymous)]
                  }
                }
              }
            }
          }
        },
        assets: {
          type: 'V5',
          value: [
            {
              id: { parents: 1, interior: { type: 'Here', value: null } },
              fun: { type: 'Fungible', value: 100000000000n }
            }
          ]
        },
        fee_asset_item: 0,
        weight_limit: { type: 'Unlimited' }
      }
    }
  },
  getEncodedData: [Function: getEncodedData],
  sign: [Function: sign],
  signSubmitAndWatch: [Function: signSubmitAndWatch],
  signAndSubmit: [Function: signAndSubmit]
}

Once the transaction is successfully included in a block, you will see the recipient's account balance updated, and you will receive output similar to the following.

???- code "Successful Transaction Submission"
    This output will be returned once the transaction has been successfully included in a block.

      ...
      {
      txHash: '0x6fbecc0b284adcff46ab39872659c2567395c865adef5f8cbea72f25b6042609',
      block: {
        index: 2,
        number: 2524809,
        hash: '0xa39a96d5921402c6e8f67e48b8395d6b21382c72d4d30f8497a0e9f890bc0d4c'
      },
      ok: true,
      events: [
        {
          type: 'Balances',
          value: {
            type: 'Withdraw',
            value: {
              who: '15DMtB5BDCJqw4uZtByTWXGqViAVx7XjRsxWbTH5tfrHLe8j',
              amount: 15668864n
            }
          },
          topics: []
        },
        {
          type: 'Balances',
          value: {
            type: 'Burned',
            value: {
              who: '15DMtB5BDCJqw4uZtByTWXGqViAVx7XjRsxWbTH5tfrHLe8j',
              amount: 100000000000n
            }
          },
          topics: []
        },
        {
          type: 'PolkadotXcm',
          value: {
            type: 'Attempted',
            value: {
              outcome: {
                type: 'Complete',
                value: { used: { ref_time: 190990000n, proof_size: 3593n } }
              }
            }
          },
          topics: []
        },
        {
          type: 'Balances',
          value: {
            type: 'Burned',
            value: {
              who: '15DMtB5BDCJqw4uZtByTWXGqViAVx7XjRsxWbTH5tfrHLe8j',
              amount: 304850000n
            }
          },
          topics: []
        },
        {
          type: 'Balances',
          value: {
            type: 'Minted',
            value: {
              who: '14xmwinmCEz6oRrFdczHKqHgWNMiCysE2KrA4jXXAAM1Eogk',
              amount: 304850000n
            }
          },
          topics: []
        },
        {
          type: 'PolkadotXcm',
          value: {
            type: 'FeesPaid',
            value: {
              paying: {
                parents: 0,
                interior: {
                  type: 'X1',
                  value: {
                    type: 'AccountId32',
                    value: {
                      network: { type: 'Polkadot', value: undefined },
                      id: FixedSizeBinary {
                        asText: [Function (anonymous)],
                        asHex: [Function (anonymous)],
                        asOpaqueHex: [Function (anonymous)],
                        asBytes: [Function (anonymous)],
                        asOpaqueBytes: [Function (anonymous)]
                      }
                    }
                  }
                }
              },
              fees: [
                {
                  id: {
                    parents: 1,
                    interior: { type: 'Here', value: undefined }
                  },
                  fun: { type: 'Fungible', value: 304850000n }
                }
              ]
            }
          },
          topics: []
        },
        {
          type: 'XcmpQueue',
          value: {
            type: 'XcmpMessageSent',
            value: {
              message_hash: FixedSizeBinary {
                asText: [Function (anonymous)],
                asHex: [Function (anonymous)],
                asOpaqueHex: [Function (anonymous)],
                asBytes: [Function (anonymous)],
                asOpaqueBytes: [Function (anonymous)]
              }
            }
          },
          topics: []
        },
        {
          type: 'PolkadotXcm',
          value: {
            type: 'Sent',
            value: {
              origin: {
                parents: 0,
                interior: {
                  type: 'X1',
                  value: {
                    type: 'AccountId32',
                    value: {
                      network: { type: 'Polkadot', value: undefined },
                      id: FixedSizeBinary {
                        asText: [Function (anonymous)],
                        asHex: [Function (anonymous)],
                        asOpaqueHex: [Function (anonymous)],
                        asBytes: [Function (anonymous)],
                        asOpaqueBytes: [Function (anonymous)]
                      }
                    }
                  }
                }
              },
              destination: {
                parents: 1,
                interior: { type: 'X1', value: { type: 'Parachain', value: 1002 } }
              },
              message: [
                {
                  type: 'ReceiveTeleportedAsset',
                  value: [
                    {
                      id: {
                        parents: 1,
                        interior: { type: 'Here', value: undefined }
                      },
                      fun: { type: 'Fungible', value: 100000000000n }
                    }
                  ]
                },
                { type: 'ClearOrigin', value: undefined },
                {
                  type: 'BuyExecution',
                  value: {
                    fees: {
                      id: {
                        parents: 1,
                        interior: { type: 'Here', value: undefined }
                      },
                      fun: { type: 'Fungible', value: 100000000000n }
                    },
                    weight_limit: { type: 'Unlimited', value: undefined }
                  }
                },
                {
                  type: 'DepositAsset',
                  value: {
                    assets: {
                      type: 'Wild',
                      value: { type: 'AllCounted', value: 1 }
                    },
                    beneficiary: {
                      parents: 0,
                      interior: {
                        type: 'X1',
                        value: {
                          type: 'AccountId32',
                          value: {
                            network: undefined,
                            id: FixedSizeBinary {
                              asText: [Function (anonymous)],
                              asHex: [Function (anonymous)],
                              asOpaqueHex: [Function (anonymous)],
                              asBytes: [Function (anonymous)],
                              asOpaqueBytes: [Function (anonymous)]
                            }
                          }
                        }
                      }
                    }
                  }
                }
              ],
              message_id: FixedSizeBinary {
                asText: [Function (anonymous)],
                asHex: [Function (anonymous)],
                asOpaqueHex: [Function (anonymous)],
                asBytes: [Function (anonymous)],
                asOpaqueBytes: [Function (anonymous)]
              }
            }
          },
          topics: []
        },
        {
          type: 'Balances',
          value: {
            type: 'Deposit',
            value: {
              who: '13UVJyLgBASGhE2ok3TvxUfaQBGUt88JCcdYjHvUhvQkFTTx',
              amount: 15668864n
            }
          },
          topics: []
        },
        {
          type: 'TransactionPayment',
          value: {
            type: 'TransactionFeePaid',
            value: {
              who: '15DMtB5BDCJqw4uZtByTWXGqViAVx7XjRsxWbTH5tfrHLe8j',
              actual_fee: 15668864n,
              tip: 0n
            }
          },
          topics: []
        },
        {
          type: 'System',
          value: {
            type: 'ExtrinsicSuccess',
            value: {
              dispatch_info: {
                weight: { ref_time: 952851000n, proof_size: 13382n },
                class: { type: 'Normal', value: undefined },
                pays_fee: { type: 'Yes', value: undefined }
              }
            }
          },
          topics: []
        }
      ]
    }

After executing the transfer, check the account balance on [Polkadot.js Apps](https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fsys.turboflakes.io%2Fasset-hub-paseo) for [Paseo's Asset Hub](https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fsys.turboflakes.io%2Fasset-hub-paseo#/accounts) and [Paseo's People Chain](https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fsys.ibp.network%2Fpeople-paseo#/accounts).

You should see:

- The recipient account now has 10 more PAS tokens.
- The sender account has the transfer amount (10 PAS) + the fees amount debited from their account balance.

You have now successfully created and sent a cross-chain transfer using the ParaSpell XCM SDK!

???- code "Full Code"

    ```typescript title="index.ts"
    import { Builder, hasDryRunSupport } from '@paraspell/sdk';
    import {
      entropyToMiniSecret,
      mnemonicToEntropy,
      ss58Address,
    } from '@polkadot-labs/hdkd-helpers';
    import { getPolkadotSigner } from 'polkadot-api/signer';
    import { sr25519CreateDerive } from '@polkadot-labs/hdkd';
    import { inspect } from 'util';

    // PAS token has 10 decimals
    const PAS_UNITS = 10_000_000_000n;

    const SEED_PHRASE =
      'INSERT_YOUR_SEED_PHRASE';

    // Create Sr25519 signer from mnemonic
    function getSigner()

    const RECIPIENT_ADDRESS = ss58Address(getSigner().publicKey);
    const SENDER_ADDRESS = ss58Address(getSigner().publicKey);

    async function transfer())
        .recipient(RECIPIENT_ADDRESS)
        .sender(SENDER_ADDRESS)
        .build();

      console.log('Built transaction:', inspect(tx, { colors: true, depth: null }));

      const result = await tx.signAndSubmit(signer);
      console.log(inspect(result, { colors: true, depth: null }));
      process.exit(0);
    }

    async function dryRunTransfer()

      const tx = await Builder()
        .from('AssetHubPaseo')
        .to('PeoplePaseo')
        .currency({
          symbol: 'PAS',
          amount: 10n * PAS_UNITS,
        })
        .recipient(RECIPIENT_ADDRESS)
        .sender(SENDER_ADDRESS)
        .dryRun();

      console.log(inspect(tx, { colors: true, depth: null }));
      process.exit(0);
    }

    dryRunTransfer();

    async function verifyED())
        .recipient(RECIPIENT_ADDRESS)
        .sender(SENDER_ADDRESS)
        .verifyEdOnDestination();

      console.log(`ED verification ${isValid ? 'successful' : 'failed'}.`);
      process.exit(0);
    }

    verifyED();

    async function XcmTransferInfo())
        .recipient(RECIPIENT_ADDRESS)
        .sender(SENDER_ADDRESS)
        .getTransferInfo();

      console.log('Transfer Info:', info);
      process.exit(0);
    }

    XcmTransferInfo();

    transfer();
    ```


Next Steps

## Next Steps

- **Read the docs**: Dive deeper into the features of the [ParaSpell XCM SDK](https://paraspell.github.io/docs/xcm-sdk/getting-started.html) documentation.

- **Learn about XCM**: Understand the underlying protocol by visiting the [Get Started with XCM](/parachains/interoperability/get-started/) guide.


Pay Transaction Fees with Different Tokens

Introduction

## Introduction

Polkadot Hub allows users to pay transaction fees using alternative tokens instead of the native token. This tutorial demonstrates how to send a DOT transfer transaction while paying the fees using USDT on Polkadot Hub.

You can follow this tutorial using [Polkadot-API (PAPI)](/reference/tools/papi/), [Polkadot.js API](/reference/tools/polkadot-js-api/), or [Subxt](/reference/tools/subxt/). Select your preferred SDK in the code tabs below.

!!! warning "Polkadot.js API Maintenance Mode"
    The Polkadot.js API is no longer actively developed. New projects should use [Polkadot-API (PAPI)](/reference/tools/papi/) or [Dedot](/reference/tools/dedot/) as actively maintained alternatives.


Prerequisites

## Prerequisites

Before starting, ensure you have the following installed:

- [Chopsticks](/reference/tools/chopsticks/) — to fork Polkadot Hub locally
- Your preferred SDK:
    - [Polkadot-API (PAPI)](/reference/tools/papi/) (TypeScript)
    - [Polkadot.js API](/reference/tools/polkadot-js-api/) (JavaScript)
    - [Subxt](/reference/tools/subxt/) (Rust)


Local Polkadot Hub Setup

## Local Polkadot Hub Setup

Fork the Polkadot Hub locally using [Chopsticks](/reference/tools/chopsticks/):

```bash
chopsticks -c polkadot-asset-hub
```

This command forks the Polkadot Hub chain, making it available at `ws://localhost:8000`. When running `polkadot-asset-hub`, you use the Polkadot Hub fork with the configuration specified in the [`polkadot-asset-hub.yml`](https://github.com/AcalaNetwork/chopsticks/blob/master/configs/polkadot-asset-hub.yml) file. This configuration defines Alice's account with USDT assets. If you want to use a different chain, ensure the account you use has the necessary assets.


Set Up Your Project

## Set Up Your Project

=== "PAPI"

    1. Create a new directory and initialize the project:

        ```bash
        mkdir fee-payment-tutorial && cd fee-payment-tutorial
        ```

    2. Initialize the project:

        ```bash
        npm init -y
        ```

    3. Install dev dependencies:

        ```bash
        npm install --save-dev @types/node@^22.12.0 ts-node@^10.9.2 typescript@^5.7.3
        ```

    4. Install dependencies:

        ```bash
        npm install --save @polkadot-labs/hdkd@0.0.28 @polkadot-labs/hdkd-helpers@0.0.29 polkadot-api@2.0.1
        ```

    5. Create TypeScript configuration:

        ```bash
        npx tsc --init && npm pkg set type=module
        ```

    6. Generate Polkadot API types for Polkadot Hub:

        ```bash
        npx papi add assetHub -n polkadot_asset_hub
        ```

    7. Create a new file called `fee-payment-transaction.ts`:

        ```bash
        touch fee-payment-transaction.ts
        ```

=== "Polkadot.js"

    1. Create a new directory and initialize the project:

        ```bash
        mkdir fee-payment-tutorial && cd fee-payment-tutorial
        ```

    2. Initialize the project:

        ```bash
        npm init -y && npm pkg set type=module
        ```

    3. Install dependencies:

        ```bash
        npm install @polkadot/api@16.5.6
        ```

    4. Create a new file called `fee-payment-transaction.js`:

        ```bash
        touch fee-payment-transaction.js
        ```

=== "Subxt"

    1. Create a new Rust project:

        ```bash
        cargo new subxt-fee-payment-example && cd subxt-fee-payment-example
        ```

    2. Install the Subxt CLI to download chain metadata:

        ```bash
        cargo install subxt-cli@0.50.0
        ```

    3. Download Polkadot Hub metadata from the local Chopsticks fork:

        ```bash
        mkdir metadata && \
        subxt metadata --url ws://localhost:8000 -o metadata/asset_hub.scale
        ```

        !!! note
            Ensure your Chopsticks fork is running at `ws://localhost:8000` before downloading the metadata.

    4. Update `Cargo.toml` with the required dependencies:

        ```toml title="Cargo.toml"
        [package]
        name = "subxt-fee-payment-example"
        version = "0.1.0"
        edition = "2021"

        [[bin]]
        name = "fee_payment_transaction"
        path = "src/bin/fee_payment_transaction.rs"

        [dependencies]
        codec = { package = "parity-scale-codec", version = "3", features = ["derive"] }
        subxt = { version = "0.50.0", features = ["jsonrpsee", "native"] }
        subxt-signer = { version = "0.50.0", features = ["sr25519"] }
        tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] }
        ```

    5. Create the source file:

        ```bash
        mkdir -p src/bin && touch src/bin/fee_payment_transaction.rs
        ```


Implementation

## Implementation

The following sections cover how to set up imports and constants, create a transaction signer, connect to Polkadot Hub, and send a DOT transfer transaction while paying fees in USDT.


### Import Dependencies and Define Constants

Set up the required imports and define the target address, transfer amount, and USDT asset ID for your transaction:

=== "PAPI"

    ```typescript title="fee-payment-transaction.ts"
    import { sr25519CreateDerive } from "@polkadot-labs/hdkd";
    import {
      DEV_PHRASE,
      entropyToMiniSecret,
      mnemonicToEntropy,
    } from "@polkadot-labs/hdkd-helpers";
    import { getPolkadotSigner } from "polkadot-api/signer";
    import { createClient } from "polkadot-api";
    import { assetHub } from "@polkadot-api/descriptors";
    import { getWsProvider } from "polkadot-api/ws";
    import { MultiAddress } from "@polkadot-api/descriptors";

    const TARGET_ADDRESS = "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3"; // Bob's address
    const TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOT
    const USDT_ASSET_ID = 1984;
    ```

=== "Polkadot.js"

    ```javascript title="fee-payment-transaction.js"
    import { ApiPromise, WsProvider } from '@polkadot/api';
    import { Keyring } from '@polkadot/keyring';
    import { cryptoWaitReady } from '@polkadot/util-crypto';

    const TARGET_ADDRESS = '14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3'; // Bob's address
    const TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOT
    const USDT_ASSET_ID = 1984;
    ```

=== "Subxt"

    In Subxt, you first generate types from the chain metadata using the `#[subxt::subxt()]` macro. The `derive_for_type` attribute ensures the `Location` type implements the traits needed for encoding. You also define a custom `AssetHubConfig` where `type AssetId = Location`, which enables specifying an XCM location as the fee payment asset:

    ```rust title="src/bin/fee_payment_transaction.rs"
    use std::str::FromStr;
    use subxt::config::{Config, DefaultExtrinsicParamsBuilder, DefaultTransactionExtensions, PolkadotConfig};
    use subxt::utils::AccountId32;
    use subxt::{OnlineClient, SubstrateConfig};

    // Generate types from the Polkadot Hub metadata with Location trait derives
    #[subxt::subxt(
        runtime_metadata_path = "metadata/asset_hub.scale",
        derive_for_type(
            path = "staging_xcm::v5::location::Location",
            derive = "Clone, Eq, PartialEq, codec::Encode",
            recursive
        )
    )]
    pub mod asset_hub {}

    // Import XCM location types from the generated metadata module
    use asset_hub::runtime_types::staging_xcm::v5::{
        junction::Junction,
        junctions::Junctions,
        location::Location,
    };

    // Define a custom config where AssetId is an XCM Location
    #[derive(Debug, Default, Clone)]
    pub struct AssetHubConfig;

    impl Config for AssetHubConfig {
        type AccountId = <PolkadotConfig as Config>::AccountId;
        type Address = <PolkadotConfig as Config>::Address;
        type Signature = <PolkadotConfig as Config>::Signature;
        type Hasher = <PolkadotConfig as Config>::Hasher;
        type Header = <SubstrateConfig as Config>::Header;
        type TransactionExtensions = DefaultTransactionExtensions<AssetHubConfig>;
        type AssetId = Location;
    }

    const POLKADOT_HUB_RPC: &str = "ws://localhost:8000";
    const TARGET_ADDRESS: &str = "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3";
    const TRANSFER_AMOUNT: u128 = 3_000_000_000; // 3 DOT
    const USDT_ASSET_ID: u128 = 1984;
    ```


Create a Signer and Connect

### Create a Signer and Connect

Create a signer using Alice's development account and connect to the local Polkadot Hub:

=== "PAPI"

    ```typescript title="fee-payment-transaction.ts"
      const entropy = mnemonicToEntropy(DEV_PHRASE);
      const miniSecret = entropyToMiniSecret(entropy);
      const derive = sr25519CreateDerive(miniSecret);
      const hdkdKeyPair = derive("//Alice");
      const polkadotSigner = getPolkadotSigner(
        hdkdKeyPair.publicKey,
        "Sr25519",
        hdkdKeyPair.sign
      );
      return polkadotSigner;
    };

    const client = createClient(
      getWsProvider("ws://localhost:8000") // Chopsticks Polkadot Hub
    );

    const api = client.getTypedApi(assetHub);

    const tx = api.tx.Balances.transfer_keep_alive({
      dest: MultiAddress.Id(TARGET_ADDRESS),
    ```

=== "Polkadot.js"

    The `cryptoWaitReady()` call ensures the underlying WASM cryptographic libraries are initialized before creating the keyring:

    ```javascript title="fee-payment-transaction.js"
    async function main());
      const alice = keyring.addFromUri('//Alice');

      // Connect to the local Chopsticks Polkadot Hub fork
      const wsProvider = new WsProvider('ws://localhost:8000');
      const api = await ApiPromise.create({ provider: wsProvider });
      console.log('Connected to Polkadot Hub (Chopsticks fork)');
    ```

=== "Subxt"

    Notice that the `OnlineClient` is parameterized with `AssetHubConfig` instead of the default `PolkadotConfig`:

    ```rust title="src/bin/fee_payment_transaction.rs"
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        // Connect to the local Chopsticks Polkadot Hub fork
        let api = OnlineClient::<AssetHubConfig>::from_url(POLKADOT_HUB_RPC).await?;
        println!("Connected to Polkadot Hub (Chopsticks fork)");

        // Anchor to the current block
        let at_block = api.at_current_block().await?;

        // Create Alice's dev keypair
        let alice = subxt_signer::sr25519::dev::alice();
        println!("Sender (Alice): {}", AccountId32::from(alice.public_key()));
    ```


Create the Transaction

### Create the Transaction

Create a standard DOT transfer transaction that sends 3 DOT to Bob's address while keeping Alice's account alive:

=== "PAPI"

    ```typescript title="fee-payment-transaction.ts"
    });

    const signer = await createSigner();
    ```

=== "Polkadot.js"

    ```javascript title="fee-payment-transaction.js"
      // Create the transfer transaction
      const tx = api.tx.balances.transferKeepAlive(TARGET_ADDRESS, TRANSFER_AMOUNT);
    ```

=== "Subxt"

    ```rust title="src/bin/fee_payment_transaction.rs"
        // Create the balance transfer transaction
        let dest = AccountId32::from_str(TARGET_ADDRESS)?;
        let tx = asset_hub::transactions()
            .balances()
            .transfer_keep_alive(dest.into(), TRANSFER_AMOUNT);
    ```


### Sign and Submit with Alternative Fee Payment

The key part of this tutorial is specifying an alternative asset for fee payment. The USDT asset is identified using the XCM location format, where `PalletInstance(50)` refers to the Assets pallet and `GeneralIndex(1984)` identifies the USDT asset on Polkadot Hub:

=== "PAPI"

    In PAPI, you specify the alternative asset through the `asset` parameter in the `signAndSubmit` options:

    ```typescript title="fee-payment-transaction.ts"
      asset: {
        parents: 0,
        interior: {
          type: "X2",
          value: [
            { type: "PalletInstance", value: 50 },
            { type: "GeneralIndex", value: BigInt(USDT_ASSET_ID) },
          ],
        },
      },
    });

    const { txHash, ok, block, events } = result;
    console.log(`Tx finalized: ${txHash} (ok=${ok})`);
    console.log(`Block: #${block.number} ${block.hash} [tx index ${block.index}]`);

    console.log("Events:");
    for (const ev of events)`);
    }

    process.exit(0);
    ```

=== "Polkadot.js"

    In Polkadot.js, you define the asset as an XCM multi-location object and pass it as the `assetId` option to `signAndSend`:

    ```javascript title="fee-payment-transaction.js"
      // Define the USDT asset as an XCM multi-location for fee payment
      const assetId = {
        parents: 0,
        interior: {
          X2: [{ PalletInstance: 50 }, { GeneralIndex: USDT_ASSET_ID }],
        },
      };

      // Sign and send the transaction, paying fees with USDT
      console.log('Signing and submitting transaction...');
      await new Promise((resolve, reject) => {
        let unsubscribe;
        tx
          .signAndSend(
            alice,
            { assetId },
            ({ status, events, txHash, dispatchError }) => {
              if (status.isFinalized)`
                );
                console.log(`Transaction hash: ${txHash.toHex()}`);

                // Display all events
                console.log('\nEvents:');
                events.forEach(({ event }) => {
                  console.log(`  ${event.section}.${event.method}`);
                });

                if (unsubscribe)

                if (dispatchError) = decoded;
                    reject(new Error(`${section}.${name}: ${docs.join(' ')}`));
                  } else {
                    reject(new Error(dispatchError.toString()));
                  }
                } else {
                  resolve();
                }
              }
            }
          )
          .then((unsub) => {
            unsubscribe = unsub;
          })
          .catch((error) => {
            if (unsubscribe)
            reject(error);
          });
      });
    ```

=== "Subxt"

    In Subxt, you use `DefaultExtrinsicParamsBuilder` with `tip_of(0, asset_location)` to specify the fee asset. The first argument is the tip amount (0), and the second is the XCM `Location`. Instead of calling `sign_and_submit_then_watch_default`, you pass the custom `tx_params` to `sign_and_submit_then_watch`:

    ```rust title="src/bin/fee_payment_transaction.rs"
        // Define the USDT asset location in XCM format
        let asset_location = Location {
            parents: 0,
            interior: Junctions::X2([
                Junction::PalletInstance(50),
                Junction::GeneralIndex(USDT_ASSET_ID),
            ]),
        };

        // Build transaction params to pay fees with the alternative asset
        let tx_params = DefaultExtrinsicParamsBuilder::<AssetHubConfig>::new()
            .tip_of(0, asset_location)
            .build();

        // Sign, submit, and watch for finalization
        println!("Signing and submitting transaction...");
        let progress = at_block
            .transactions()
            .sign_and_submit_then_watch(&tx, &alice, tx_params)
            .await?;

        let in_block = progress.wait_for_finalized().await?;
        let block_hash = in_block.block_hash();
        let events = in_block.wait_for_success().await?;

        // Display transaction results
        println!("\nTransaction finalized in block: {:?}", block_hash);

        println!("\nEvents:");
        for event in events.iter().{}",
                event.pallet_name(),
                event.event_name()
            );
        }
    ```


Full Code

### Full Code

=== "PAPI"

    ??? code "Complete Code"

        ```typescript title="fee-payment-transaction.ts"
        import { sr25519CreateDerive } from "@polkadot-labs/hdkd";
        import {
          DEV_PHRASE,
          entropyToMiniSecret,
          mnemonicToEntropy,
        } from "@polkadot-labs/hdkd-helpers";
        import { getPolkadotSigner } from "polkadot-api/signer";
        import { createClient } from "polkadot-api";
        import { assetHub } from "@polkadot-api/descriptors";
        import { getWsProvider } from "polkadot-api/ws";
        import { MultiAddress } from "@polkadot-api/descriptors";

        const TARGET_ADDRESS = "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3"; // Bob's address
        const TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOT
        const USDT_ASSET_ID = 1984;

        const createSigner = async () => {
          const entropy = mnemonicToEntropy(DEV_PHRASE);
          const miniSecret = entropyToMiniSecret(entropy);
          const derive = sr25519CreateDerive(miniSecret);
          const hdkdKeyPair = derive("//Alice");
          const polkadotSigner = getPolkadotSigner(
            hdkdKeyPair.publicKey,
            "Sr25519",
            hdkdKeyPair.sign
          );
          return polkadotSigner;
        };

        const client = createClient(
          getWsProvider("ws://localhost:8000") // Chopsticks Polkadot Hub
        );

        const api = client.getTypedApi(assetHub);

        const tx = api.tx.Balances.transfer_keep_alive({
          dest: MultiAddress.Id(TARGET_ADDRESS),
          value: BigInt(TRANSFER_AMOUNT),
        });

        const signer = await createSigner();

        const result = await tx.signAndSubmit(signer, {
          asset: {
            parents: 0,
            interior: {
              type: "X2",
              value: [
                { type: "PalletInstance", value: 50 },
                { type: "GeneralIndex", value: BigInt(USDT_ASSET_ID) },
              ],
            },
          },
        });

        const { txHash, ok, block, events } = result;
        console.log(`Tx finalized: ${txHash} (ok=${ok})`);
        console.log(`Block: #${block.number} ${block.hash} [tx index ${block.index}]`);

        console.log("Events:");
        for (const ev of events)`);
        }

        process.exit(0);
        ```

=== "Polkadot.js"

    ??? code "Complete Code"

        ```javascript title="fee-payment-transaction.js"
        import { ApiPromise, WsProvider } from '@polkadot/api';
        import { Keyring } from '@polkadot/keyring';
        import { cryptoWaitReady } from '@polkadot/util-crypto';

        const TARGET_ADDRESS = '14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3'; // Bob's address
        const TRANSFER_AMOUNT = 3_000_000_000n; // 3 DOT
        const USDT_ASSET_ID = 1984;

        async function main());
          const alice = keyring.addFromUri('//Alice');

          // Connect to the local Chopsticks Polkadot Hub fork
          const wsProvider = new WsProvider('ws://localhost:8000');
          const api = await ApiPromise.create({ provider: wsProvider });
          console.log('Connected to Polkadot Hub (Chopsticks fork)');

          // Create the transfer transaction
          const tx = api.tx.balances.transferKeepAlive(TARGET_ADDRESS, TRANSFER_AMOUNT);

          // Define the USDT asset as an XCM multi-location for fee payment
          const assetId = {
            parents: 0,
            interior: {
              X2: [{ PalletInstance: 50 }, { GeneralIndex: USDT_ASSET_ID }],
            },
          };

          // Sign and send the transaction, paying fees with USDT
          console.log('Signing and submitting transaction...');
          await new Promise((resolve, reject) => {
            let unsubscribe;
            tx
              .signAndSend(
                alice,
                { assetId },
                ({ status, events, txHash, dispatchError }) => {
                  if (status.isFinalized)`
                    );
                    console.log(`Transaction hash: ${txHash.toHex()}`);

                    // Display all events
                    console.log('\nEvents:');
                    events.forEach(({ event }) => {
                      console.log(`  ${event.section}.${event.method}`);
                    });

                    if (unsubscribe)

                    if (dispatchError) = decoded;
                        reject(new Error(`${section}.${name}: ${docs.join(' ')}`));
                      } else {
                        reject(new Error(dispatchError.toString()));
                      }
                    } else {
                      resolve();
                    }
                  }
                }
              )
              .then((unsub) => {
                unsubscribe = unsub;
              })
              .catch((error) => {
                if (unsubscribe)
                reject(error);
              });
          });

          // Disconnect from the node
          await api.disconnect();
        }

        main().catch(console.error);
        ```

=== "Subxt"

    ??? code "Complete Code"

        ```rust title="src/bin/fee_payment_transaction.rs"
        use std::str::FromStr;
        use subxt::config::{Config, DefaultExtrinsicParamsBuilder, DefaultTransactionExtensions, PolkadotConfig};
        use subxt::utils::AccountId32;
        use subxt::{OnlineClient, SubstrateConfig};

        // Generate types from the Polkadot Hub metadata with Location trait derives
        #[subxt::subxt(
            runtime_metadata_path = "metadata/asset_hub.scale",
            derive_for_type(
                path = "staging_xcm::v5::location::Location",
                derive = "Clone, Eq, PartialEq, codec::Encode",
                recursive
            )
        )]
        pub mod asset_hub {}

        // Import XCM location types from the generated metadata module
        use asset_hub::runtime_types::staging_xcm::v5::{
            junction::Junction,
            junctions::Junctions,
            location::Location,
        };

        // Define a custom config where AssetId is an XCM Location
        #[derive(Debug, Default, Clone)]
        pub struct AssetHubConfig;

        impl Config for AssetHubConfig {
            type AccountId = <PolkadotConfig as Config>::AccountId;
            type Address = <PolkadotConfig as Config>::Address;
            type Signature = <PolkadotConfig as Config>::Signature;
            type Hasher = <PolkadotConfig as Config>::Hasher;
            type Header = <SubstrateConfig as Config>::Header;
            type TransactionExtensions = DefaultTransactionExtensions<AssetHubConfig>;
            type AssetId = Location;
        }

        const POLKADOT_HUB_RPC: &str = "ws://localhost:8000";
        const TARGET_ADDRESS: &str = "14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3";
        const TRANSFER_AMOUNT: u128 = 3_000_000_000; // 3 DOT
        const USDT_ASSET_ID: u128 = 1984;

        #[tokio::main]
        async fn main() -> Result<(), Box<dyn std::error::Error>> {
            // Connect to the local Chopsticks Polkadot Hub fork
            let api = OnlineClient::<AssetHubConfig>::from_url(POLKADOT_HUB_RPC).await?;
            println!("Connected to Polkadot Hub (Chopsticks fork)");

            // Anchor to the current block
            let at_block = api.at_current_block().await?;

            // Create Alice's dev keypair
            let alice = subxt_signer::sr25519::dev::alice();
            println!("Sender (Alice): {}", AccountId32::from(alice.public_key()));

            // Create the balance transfer transaction
            let dest = AccountId32::from_str(TARGET_ADDRESS)?;
            let tx = asset_hub::transactions()
                .balances()
                .transfer_keep_alive(dest.into(), TRANSFER_AMOUNT);

            // Define the USDT asset location in XCM format
            let asset_location = Location {
                parents: 0,
                interior: Junctions::X2([
                    Junction::PalletInstance(50),
                    Junction::GeneralIndex(USDT_ASSET_ID),
                ]),
            };

            // Build transaction params to pay fees with the alternative asset
            let tx_params = DefaultExtrinsicParamsBuilder::<AssetHubConfig>::new()
                .tip_of(0, asset_location)
                .build();

            // Sign, submit, and watch for finalization
            println!("Signing and submitting transaction...");
            let progress = at_block
                .transactions()
                .sign_and_submit_then_watch(&tx, &alice, tx_params)
                .await?;

            let in_block = progress.wait_for_finalized().await?;
            let block_hash = in_block.block_hash();
            let events = in_block.wait_for_success().await?;

            // Display transaction results
            println!("\nTransaction finalized in block: {:?}", block_hash);

            println!("\nEvents:");
            for event in events.iter().{}",
                    event.pallet_name(),
                    event.event_name()
                );
            }

            Ok(())
        }
        ```


Run the Script

### Run the Script

=== "PAPI"

    ```bash
    npx ts-node fee-payment-transaction.ts
    ```

=== "Polkadot.js"

    ```bash
    node fee-payment-transaction.js
    ```

=== "Subxt"

    ```bash
    cargo run --bin fee_payment_transaction
    ```


Expected Output

### Expected Output

When you run the script successfully, you should see output similar to:

=== "PAPI"

      npx ts-node fee-payment-transaction.ts

    Tx finalized: 0xfe4e3fa64d374e256c72463c507743f16672caaf1b4e539fe913026de394009e (ok=true)
    Block: #12255461 0xaf315c306304ad175e4e24c5c8cbf97518c1411183bbf81a6107209a49d84f4d [tx index 2]
    Events:
    - Assets
    - Balances
    - Assets
    - AssetConversion
    - Balances
    - System
    - Balances
    - Balances
    - Balances
    - AssetTxPayment
    - System

=== "Polkadot.js"

      node fee-payment-transaction.js

    Connected to Polkadot Hub (Chopsticks fork)
    Signing and submitting transaction...

    Transaction finalized in block: 0x1f4849218bb4c04564a6c6f69c9e40a3940dcdabdc089da01bb49fb471a2c049
    Transaction hash: 0x9c967bb79fd09579f5e530a0446ce0171efe9241ba5957d6bcba80bccd5f66da

    Events:
      assets.Withdrawn
      balances.Withdraw
      assets.Deposited
      assetConversion.SwapCreditExecuted
      balances.Upgraded
      system.NewAccount
      balances.Endowed
      balances.Transfer
      balances.Deposit
      assetTxPayment.AssetTxFeePaid
      system.ExtrinsicSuccess

=== "Subxt"

      cargo run --bin fee_payment_transaction

    Connected to Polkadot Hub (Chopsticks fork)
    Sender (Alice): 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
    Signing and submitting transaction...

    Transaction finalized in block: 0x90c92e4dca64631ab4ccaabb14273cebbb3d59205db437dfcf9ace91452b1434

    Events:
      Assets.Withdrawn
      Balances.Withdraw
      Assets.Deposited
      AssetConversion.SwapCreditExecuted
      Balances.Upgraded
      System.NewAccount
      Balances.Endowed
      Balances.Transfer
      Balances.Deposit
      AssetTxPayment.AssetTxFeePaid
      System.ExtrinsicSuccess

The key events to look for are:

- **`AssetTxPayment`**: Confirms the fees were paid using the alternative asset.
- **`AssetConversion`**: The alternative asset was swapped to cover native fees.
- **`Balances`**: The transfer was executed and deposit and withdrawal events were emitted.
- **`System`**: The transaction completed successfully.


Conclusion

## Conclusion

Paying transaction fees with alternative tokens on Polkadot Hub provides significant flexibility for users and applications.

The key takeaway is understanding how to specify alternative assets using the XCM location format, which opens up possibilities for building applications that can operate entirely using specific token ecosystems while still leveraging the full power of the network.


Where to Go Next

## Where to Go Next

-   Guide __Send Transactions with SDKs__

    ---

    Learn how to send various types of transactions using different SDKs.

    [:octicons-arrow-right-24: Get Started](/chain-interactions/send-transactions/with-sdks/)

-   Guide __Calculate Transaction Fees__

    ---

    Understand how to estimate transaction fees before submitting.

    [:octicons-arrow-right-24: Get Started](/chain-interactions/send-transactions/calculate-transaction-fees/)


Introduction

## Introduction

Sending transactions on Polkadot SDK-based blockchains involves constructing an extrinsic (transaction), signing it with your account's private key, and submitting it to the network. Each SDK provides different methods for transaction construction, signing, and submission.

This guide demonstrates how to send transactions using five popular SDKs:

- **[Polkadot API (PAPI)](/reference/tools/papi/)**: Modern TypeScript library with type-safe APIs
- **[Polkadot.js API](/reference/tools/polkadot-js-api/)**: Comprehensive JavaScript library (maintenance mode)
- **[Dedot](/reference/tools/dedot/)**: Lightweight TypeScript library optimized for performance
- **[Python Substrate Interface](/reference/tools/py-substrate-interface/)**: Python library for Substrate chains
- **[Subxt](/reference/tools/subxt/)**: Rust library with compile-time type safety

Select your preferred SDK below to see complete, runnable examples that send balance transfer transactions on Polkadot Hub.


Prerequisites

## Prerequisites

- Access to a Polkadot-SDK-compatible wallet, with its mnemonic phrase or private key.
- A funded account on Polkadot Hub, with some testnet tokens. You can use the [Polkadot Faucet](https://faucet.polkadot.io/?parachain=1111) to obtain test tokens.


Send Transactions

## Send Transactions

!!! warning
    Never share your mnemonic phrase or private keys. The examples below use mnemonics for demonstration purposes only. In production, use secure key management solutions.

=== "PAPI"

    **Prerequisites**

    - [Node.js](https://nodejs.org/) v18 or higher
    - npm, pnpm, or yarn package manager

    **Environment Setup**

    1. Create and initialize a new project:

        ```bash
        mkdir papi-send-tx-example && cd papi-send-tx-example && \
        npm init -y && npm pkg set type=module
        ```

    2. Install dependencies:

        ```bash
        npm install polkadot-api @polkadot/util-crypto @polkadot/keyring && \
        npm install --save-dev @types/node tsx typescript
        ```

    3. Generate types for Polkadot Hub TestNet:

        ```bash
        npx papi add polkadotTestNet -w wss://asset-hub-paseo.dotters.network
        ```

    **Send Balance Transfer**

    The following example constructs, signs, and submits a balance transfer transaction.

    Create a file named `send-transfer.ts` and add the following code to it:

    ```typescript title="send-transfer.ts"
    import { createClient } from 'polkadot-api';
    import { getWsProvider } from 'polkadot-api/ws';
    import { polkadotTestNet } from '@polkadot-api/descriptors';
    import { cryptoWaitReady } from '@polkadot/util-crypto';
    import { Keyring } from '@polkadot/keyring';
    import { getPolkadotSigner } from 'polkadot-api/signer';

    const POLKADOT_TESTNET_RPC = 'INSERT_WS_ENDPOINT';
    const SENDER_MNEMONIC = 'INSERT_MNEMONIC';
    const DEST_ADDRESS = 'INSERT_DEST_ADDRESS';
    const AMOUNT = 1_000_000_000n; // 1 PAS (adjust decimals as needed)

    async function main());
        const sender = keyring.addFromMnemonic(SENDER_MNEMONIC);

        console.log(`Sender address: ${sender.address}`);

        // Create the client connection
        const client = createClient(getWsProvider(POLKADOT_TESTNET_RPC));

        // Get the typed API
        const api = client.getTypedApi(polkadotTestNet);
        console.log('Connected to Polkadot Testnet');

        // Create signer using getPolkadotSigner
        const signer = getPolkadotSigner(
          sender.publicKey,
          'Sr25519',
          async (input) => sender.sign(input)
        );

        // Construct and submit the transfer transaction
        const tx = api.tx.Balances.transfer_keep_alive({
          dest: { type: 'Id', value: DEST_ADDRESS },
          value: AMOUNT,
        });

        console.log('\nSigning and submitting transaction...');
        const { txHash } = await tx.signAndSubmit(signer);
        console.log(`Transaction submitted with hash: ${txHash}`);

        await client.destroy();
        console.log('Disconnected');
      } catch (error)
    }

    main();
    ```

    !!! note
        Ensure to replace `INSERT_WS_ENDPOINT` with the proper WebSocket endpoint, `INSERT_SENDER_MNEMONIC` with your account's mnemonic phrase, and `INSERT_DEST_ADDRESS` with the recipient address. For this example, you can use Polkadot Hub (`wss://polkadot-asset-hub-rpc.polkadot.io`).

    Run the script:

    ```bash
    npx tsx send-transfer.ts
    ```

    You should see output similar to:

        npx tsx send-transfer.ts
        Sender address: 5GgbDVeKZwCmMHzn58iFSgSZDTojRMM52arXnuNXto28R7mg
        Connected to Polkadot Testnet
        Signing and submitting transaction...
        Transaction submitted with hash: 0x45ce4b5223428d003ddeed5118182eede1224dd0b6e881d7ed40517d60aa1c57
        Disconnected

=== "Polkadot.js"

    !!! warning "Maintenance Mode Only"
        The Polkadot.js API is no longer actively developed. New projects should use [PAPI](/reference/tools/papi/) or [Dedot](/reference/tools/dedot/) as actively maintained alternatives.

    **Prerequisites**

    - [Node.js](https://nodejs.org/) v18 or higher
    - npm, pnpm, or yarn package manager

    **Environment Setup**

    1. Create and initialize a new project:

        ```bash
        mkdir pjs-send-tx-example && cd pjs-send-tx-example && \
        npm init -y && npm pkg set type=module
        ```

    2. Install dependencies:

        ```bash
        npm install @polkadot/api @polkadot/keyring @polkadot/util-crypto
        ```

    **Send Balance Transfer**

    The following example constructs, signs, and submits a balance transfer transaction.

    Create a file named `send-transfer.js` and add the following code:

    ```javascript title="send-transfer.js"
    import { ApiPromise, WsProvider } from '@polkadot/api';
    import { Keyring } from '@polkadot/keyring';
    import { cryptoWaitReady } from '@polkadot/util-crypto';

    const POLKADOT_TESTNET_RPC = 'INSERT_WS_ENDPOINT';
    const SENDER_MNEMONIC = 'INSERT_MNEMONIC';
    const DEST_ADDRESS = 'INSERT_DEST_ADDRESS';
    const AMOUNT = 1_000_000_000n; // 1 PAS (adjust decimals as needed)

    async function main());
      console.log('Connected to Polkadot Testnet');

      // Set up keyring and get sender account
      const keyring = new Keyring({ type: 'sr25519' });
      const sender = keyring.addFromMnemonic(SENDER_MNEMONIC);
      const senderAddress = sender.address;

      console.log(`Sender address: ${senderAddress}`);
      console.log(`Recipient address: ${DEST_ADDRESS}`);
      console.log(`Amount: ${AMOUNT} (${Number(AMOUNT) / 1_000_000_000} PAS)\n`);

      // Get sender's account info to check balance
      const accountInfo = await api.query.system.account(senderAddress);
      console.log(
        `Sender balance: ${accountInfo.data.free.toString()} (${
          Number(accountInfo.data.free.toBigInt()) / 1_000_000_000
        } PAS)`
      );

      // Construct and sign the transfer transaction
      console.log('\nSigning and submitting transaction...');

      await new Promise((resolve, reject) => {
        api.tx.balances
          .transferKeepAlive(DEST_ADDRESS, AMOUNT)
          .signAndSend(sender, ({ status, txHash, dispatchError }) => {
            if (status.isInBlock)`
              );
            } else if (status.isFinalized)`
              );
              console.log(`Transaction hash: ${txHash.toHex()}`);

              if (dispatchError) = decoded;
                  reject(new Error(`${section}.${name}: ${docs.join(' ')}`));
                } else {
                  reject(new Error(dispatchError.toString()));
                }
              } else {
                console.log('Transaction successful!');
                resolve(txHash.toHex());
              }
            }
          })
          .catch(reject);
      });

      // Disconnect from the node
      await api.disconnect();
    }

    main().catch(console.error);
    ```

    !!! note
        Ensure to replace `INSERT_WS_ENDPOINT` with the proper WebSocket endpoint, `INSERT_SENDER_MNEMONIC` with your account's mnemonic phrase, and `INSERT_DEST_ADDRESS` with the recipient address. For this example, you can use Polkadot Hub (`wss://polkadot-asset-hub-rpc.polkadot.io`).

    Run the script:

    ```bash
    node send-transfer.js
    ```

    You should see output similar to:

        node send-transfer.js
        Connected to Polkadot Testnet
        Sender address: 5GgbDVeKZwCmMHzn58iFSgSZDTojRMM52arXnuNXto28R7mg
        Recipient address: 5GgbDVeKZwCmMHzn58iFSgSZDTojRMM52arXnuNXto28R7mg
        Amount: 1000000000 (1 PAS)

        Sender balance: 59912386816 (59.912386816 PAS)

        Signing and submitting transaction...
        Transaction included in block: 0x77b5d73d30412ec39ac6cfe6cf3ec2134fec8e3fcbcd821218863d01f9fac40f
        Transaction finalized in block: 0x77b5d73d30412ec39ac6cfe6cf3ec2134fec8e3fcbcd821218863d01f9fac40f
        Transaction hash: 0x6f2fbe51985c87a5534c919d76859b66138aaf874147a1e300e50d93cee1c429
        Transaction successful!

=== "Dedot"

    **Prerequisites**

    - [Node.js](https://nodejs.org/) v18 or higher
    - npm, pnpm, or yarn package manager

    **Environment Setup**

    1. Create and initialize a new project:

        ```bash
        mkdir dedot-send-tx-example && cd dedot-send-tx-example && \
        npm init -y && npm pkg set type=module
        ```

    2. Install dependencies:

        ```bash
        npm install dedot @polkadot/keyring @polkadot/util-crypto && \
        npm install --save-dev @dedot/chaintypes @types/node tsx typescript
        ```

    **Send Balance Transfer**

    The following example constructs, signs, and submits a balance transfer transaction.

    Create a file named `send-transfer.ts` and add the following code to it:

    ```typescript title="send-transfer.ts"
    import { DedotClient, WsProvider } from 'dedot';
    import type { PolkadotAssetHubApi } from '@dedot/chaintypes';
    import { cryptoWaitReady } from '@polkadot/util-crypto';
    import { Keyring } from '@polkadot/keyring';

    const POLKADOT_TESTNET_RPC = 'INSERT_WS_ENDPOINT';
    const SENDER_MNEMONIC = 'INSERT_MNEMONIC';
    const DEST_ADDRESS = 'INSERT_DEST_ADDRESS';
    const AMOUNT = 1_000_000_000n; // 1 PAS (adjust decimals as needed)

    async function main());
      const sender = keyring.addFromMnemonic(SENDER_MNEMONIC);
      const senderAddress = sender.address;

      console.log(`Sender address: ${senderAddress}`);
      console.log(`Recipient address: ${DEST_ADDRESS}`);
      console.log(`Amount: ${AMOUNT} (${AMOUNT / 1_000_000_000n} PAS)\n`);

      // Get sender's account info to check balance
      const accountInfo = await client.query.system.account(senderAddress);
      console.log(`Sender balance: ${accountInfo.data.free}`);

      // Sign and submit the transfer transaction
      console.log('\nSigning and submitting transaction...');

      // Wait for transaction to complete using a Promise
      await new Promise<void>((resolve, reject) => {
        client.tx.balances
          .transferKeepAlive(DEST_ADDRESS, AMOUNT)
          .signAndSend(sender, async ({ status, txHash, dispatchError }) => {
            console.log(`Transaction status: ${status.type}`);

            // Log transaction hash immediately
            if (txHash)`
              );
            }

            if (status.type === 'BestChainBlockIncluded')`
              );
            }

            if (status.type === 'Finalized')`
              );

              // Check for dispatch errors
              if (dispatchError).${decoded.name}: ${decoded.docs}`
                  );
                  reject(
                    new Error(
                      `Transaction failed: ${decoded.section}.${decoded.name}`
                    )
                  );
                } else {
                  console.error(`Dispatch error: ${dispatchError.type}`);
                  reject(new Error(`Transaction failed: ${dispatchError.type}`));
                }
              } else {
                console.log('Transaction successful!');
                resolve();
              }
            }
          })
          .catch(reject);
      });

      // Disconnect the client after transaction completes
      await client.disconnect();
      console.log('Disconnected from Polkadot Hub');
    }

    main().catch(console.error);
    ```

    !!! note
        Ensure to replace `INSERT_WS_ENDPOINT` with the proper WebSocket endpoint, `INSERT_SENDER_MNEMONIC` with your account's mnemonic phrase, and `INSERT_DEST_ADDRESS` with the recipient address. For this example, you can use Polkadot Hub (`wss://polkadot-asset-hub-rpc.polkadot.io`).

    Run the script:

    ```bash
    npx tsx send-transfer.ts
    ```

    You should see output similar to:

        npx tsx send-transfer.ts
        Connected to Polkadot Testnet
        Sender address: 5GgbDVeKZwCmMHzn58iFSgSZDTojRMM52arXnuNXto28R7mg
        Recipient address: 5GgbDVeKZwCmMHzn58iFSgSZDTojRMM52arXnuNXto28R7mg
        Amount: 1000000000 (1 PAS)

        Sender balance: 59868680224

        Signing and submitting transaction...
        Transaction status: Validated
        Transaction status: Broadcasting
        Transaction status: BestChainBlockIncluded
        Transaction included in block: 0x80b039e897d8cfe4ec0c641cd17cc7a47ed4b26797b31c7d3c93c3b0b96f7b9f
        Transaction status: Finalized
        Transaction hash: 0x325a1c1cff76fb6a004190cf5ee382f89433596b3c396f10fd25ce6945f2b1df
        Transaction finalized in block: 0x80b039e897d8cfe4ec0c641cd17cc7a47ed4b26797b31c7d3c93c3b0b96f7b9f
        Transaction successful!
        Disconnected from Polkadot Testnet

=== "Python Substrate Interface"

    **Prerequisites**

    - [Python](https://www.python.org/) 3.8 or higher
    - pip package manager

    **Environment Setup**

    1. Create a new project directory and set up a virtual environment:

        ```bash
        mkdir psi-send-tx-example && cd psi-send-tx-example && \
        python3 -m venv venv && source venv/bin/activate
        ```

    2. Install the substrate-interface package:

        ```bash
        pip install substrate-interface
        ```

    **Send Balance Transfer**

    The following example constructs, signs, and submits a balance transfer transaction.

    Create a file named `send_transfer.py` and add the following code to it:

    ```python title="send_transfer.py"
    from substrateinterface import SubstrateInterface, Keypair

    POLKADOT_TESTNET_RPC = "INSERT_WS_ENDPOINT"
    SENDER_MNEMONIC = "INSERT_MNEMONIC"
    DEST_ADDRESS = "INSERT_DEST_ADDRESS"
    AMOUNT = 1_000_000_000  # 1 PAS (adjust decimals as needed)

    def main():
        # Connect to Polkadot Hub
        substrate = SubstrateInterface(url=POLKADOT_TESTNET_RPC)

        print("Connected to Polkadot Testnet")

        # Create keypair from mnemonic
        keypair = Keypair.create_from_mnemonic(SENDER_MNEMONIC)
        sender_address = keypair.ss58_address

        print(f"Sender address: {sender_address}")
        print(f"Recipient address: {DEST_ADDRESS}")
        print(f"Amount: {AMOUNT} ({AMOUNT / 1_000_000_000} PAS)\n")

        # Get sender's account info to check balance
        account_info = substrate.query(
            module="System", storage_function="Account", params=[sender_address]
        )
        print(f"Sender balance: {account_info.value['data']['free']}")

        # Compose the transfer call
        call = substrate.compose_call(
            call_module="Balances",
            call_function="transfer_keep_alive",
            call_params={"dest": DEST_ADDRESS, "value": AMOUNT},
        )

        # Create a signed extrinsic
        print("\nSigning and submitting transaction...")
        extrinsic = substrate.create_signed_extrinsic(call=call, keypair=keypair)

        # Submit and wait for inclusion
        receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)

        if receipt.is_success:
            print("\nTransaction successful!")
            print(f"Extrinsic Hash: {receipt.extrinsic_hash}")
            print(f"Block Hash: {receipt.block_hash}")
        else:
            print(f"\nTransaction failed: {receipt.error_message}")

        # Close connection
        substrate.close()

    if __name__ == "__main__":
        main()
    ```

    !!! note
        Ensure to replace `INSERT_WS_ENDPOINT` with the proper WebSocket endpoint, `INSERT_SENDER_MNEMONIC` with your account's mnemonic phrase, and `INSERT_DEST_ADDRESS` with the recipient address. For this example, you can use Polkadot Hub (`wss://polkadot-asset-hub-rpc.polkadot.io`).

    Run the script:

    ```bash
    python send_transfer.py
    ```

    You should see output similar to:

        python send_transfer.py
        Connected to Polkadot Testnet
        Sender address: 5GgbDVeKZwCmMHzn58iFSgSZDTojRMM52arXnuNXto28R7mg
        Recipient address: 5GgbDVeKZwCmMHzn58iFSgSZDTojRMM52arXnuNXto28R7mg
        Amount: 1000000000 (1.0 PAS)

        Sender balance: 59854111360

        Signing and submitting transaction...
        Transaction successful!
        Extrinsic Hash: 0x5509e26874f5274747896349be68b0904058f4e2822385fdb7c8dc86f2dab879
        Block Hash: 0xfd79ad0e21c5a91c358e2bd59540a98681e407e4231c9577221f97cc121449b8

=== "Subxt"

    [Subxt](/reference/tools/subxt/) is a Rust library that provides compile-time type safety through code generation from chain metadata.

    **Prerequisites**

    - [Rust](https://rustup.rs/) toolchain (latest stable)
    - Cargo package manager

    **Environment Setup**

    1. Create a new Rust project:

        ```bash
        cargo new subxt-send-tx-example && cd subxt-send-tx-example
        ```

    2. Install the Subxt CLI:

        ```bash
        cargo install subxt-cli@0.50.0
        ```

    3. Download the Polkadot Hub metadata:

        ```bash
        subxt metadata --url INSERT_WS_ENDPOINT -o asset_hub_metadata.scale
        ```

    4. Update `Cargo.toml` with the required dependencies:

        ```toml title="Cargo.toml"
        [package]
        name = "subxt-send-tx-example"
        version = "0.1.0"
        edition = "2021"

        [[bin]]
        name = "send_transfer"
        path = "src/bin/send_transfer.rs"

        [dependencies]
        subxt = { version = "0.50.0", features = ["native"] }
        subxt-signer = "0.50.0"
        tokio = { version = "1", features = ["rt", "macros"] }

        ```

    **Send Balance Transfer**

    The following example constructs, signs, and submits a balance transfer transaction.

    Create a file at `src/bin/send_transfer.rs` and add the following code to it:

    ```rust title="src/bin/send_transfer.rs"
    use std::str::FromStr;
    use subxt::utils::AccountId32;
    use subxt::{OnlineClient, PolkadotConfig};
    use subxt_signer::{bip39::Mnemonic, sr25519::Keypair};

    // Generate an interface from the node's metadata
    #[subxt::subxt(runtime_metadata_path = "asset_hub_metadata.scale")]
    pub mod asset_hub {}

    const ASSET_HUB_RPC: &str = "INSERT_WS_ENDPOINT";
    const SENDER_MNEMONIC: &str = "INSERT_SENDER_MNEMONIC";
    const DEST_ADDRESS: &str = "INSERT_DEST_ADDRESS";
    const AMOUNT: u128 = 1_000_000_000_000; // 1 DOT (12 decimals)

    #[tokio::main(flavor = "current_thread")]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        // Initialize the Subxt client
        let api = OnlineClient::<PolkadotConfig>::from_url(ASSET_HUB_RPC).await?;

        // Anchor to the current block
        let at_block = api.at_current_block().await?;

        println!("Connected to Polkadot Hub");

        // Load the sender's keypair from a mnemonic phrase
        let mnemonic = Mnemonic::parse(SENDER_MNEMONIC)?;
        let sender_keypair = Keypair::from_phrase(&mnemonic, None)?;
        let sender_address = AccountId32::from(sender_keypair.public_key());

        println!("Sender address: {}", sender_address);
        println!("Recipient address: {}", DEST_ADDRESS);
        println!("Amount: {} ({} DOT)\n", AMOUNT, AMOUNT / 1_000_000_000_000);

        // Get sender's account info to check balance
        let account_storage = at_block
            .storage()
            .entry(asset_hub::storage().system().account())?;
        let account_info = account_storage
            .fetch((sender_address,))
            .await?
            .decode()?;

        println!("Sender balance: {}", account_info.data.free);

        // Convert the recipient address into an AccountId32
        let dest = AccountId32::from_str(DEST_ADDRESS)?;

        // Build the balance transfer extrinsic
        let balance_transfer_tx = asset_hub::transactions()
            .balances()
            .transfer_keep_alive(dest.into(), AMOUNT);

        // Sign and submit the extrinsic, then wait for it to be finalized
        println!("\nSigning and submitting transaction...");
        let events = at_block
            .transactions()
            .sign_and_submit_then_watch_default(&balance_transfer_tx, &sender_keypair)
            .await?
            .wait_for_finalized_success()
            .await?;

        // Check for a successful transfer event
        if let Some(event) = events.find_first::<asset_hub::balances::events::Transfer>()", event);
        }

        Ok(())
    }
    ```

    !!! note
        Ensure to replace `INSERT_WS_ENDPOINT` with the proper WebSocket endpoint, `INSERT_SENDER_MNEMONIC` with your account's mnemonic phrase, and `INSERT_DEST_ADDRESS` with the recipient address. For this example, you can use Polkadot Hub (`wss://polkadot-asset-hub-rpc.polkadot.io`).

    Run the script:

    ```bash
    cargo run --bin send_transfer
    ```

    You should see output similar to:

        cargo run --bin send_transfer
        Connected to Polkadot Hub
        Sender address: 14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3
        Recipient address: 15uPcYeUE2XaMiMJuR6W7QGW2LsLdKXX7F3PxKG8gcizPh3X
        Amount: 1000000000000 (1 DOT)

        Sender balance: 50000000000000

        Signing and submitting transaction...

        Transaction successful!
        Transfer event: Transfer { from: 14E5nqKAp3oAJcmzgZhUD2RcptBeUBScxKHgJKU4HPNcKVf3, to: 15uPcYeUE2XaMiMJuR6W7QGW2LsLdKXX7F3PxKG8gcizPh3X, amount: 1000000000000 }


Where to Go Next

## Where to Go Next

- Guide **Query On-Chain State**

    ---

    Learn how to query storage and runtime data with the SDKs used in this guide.

    [:octicons-arrow-right-24: Get Started](/chain-interactions/query-data/query-sdks/)

- Guide **Calculate Transaction Fees**

    ---

    Estimate fees before sending transactions

    [:octicons-arrow-right-24: Get Started](/chain-interactions/send-transactions/calculate-transaction-fees/)


Store and Retrieve Data on the Bulletin Chain

Prerequisites

## Prerequisites

- A Polkadot account (SS58 format) — see [Create an Account](/chain-interactions/accounts/create-account/) if you need one
- A browser wallet extension (Polkadot.js, Talisman, SubWallet, or Fearless)
- An image or file to store (under ~8 MiB per transaction; for larger files, you can chunk your data — see [Size Limits](/reference/polkadot-hub/data-storage/#size-limits))
- Authorization to store data on the Bulletin Chain (covered in the next section)
- For the PAPI method: [Node.js](https://nodejs.org/) v18 or higher


Get Authorization

## Get Authorization

The Bulletin Chain has no token balances — you need authorization before you can store data. Authorization grants your account a specific number of transactions and bytes that you can use for storage.

For this tutorial, we're going to use the testnet faucet for authorizations.

=== "Console UI"

    1. Navigate to the [Bulletin Chain Console](https://paritytech.github.io/polkadot-bulletin-chain/) and click **Connect** to connect your wallet.

        ![](/images/chain-interactions/store-data/bulletin-chain/bulletin-chain-1.webp)

    2. Go to the **Faucet** page and select the **Storage Faucet** tab.
    3. Under **Authorize Account**, enter the desired number of **Transactions** and **Bytes** for your storage needs, then click **Authorize Account**.

        ![](/images/chain-interactions/store-data/bulletin-chain/bulletin-chain-2.webp)

    4. Approve the transaction in your wallet extension. You should see a success confirmation.

        ![](/images/chain-interactions/store-data/bulletin-chain/bulletin-chain-3.webp)

    5. To verify your authorization, switch to the **Accounts** tab to view your remaining transactions, bytes, and expiration block.

        ![](/images/chain-interactions/store-data/bulletin-chain/bulletin-chain-4.webp)

    !!! note
        Authorization has an expiration block. Once expired, unused authorization is not refunded — you'll need to request new authorization.

!!! note
    The `authorize_account` extrinsic requires Root origin (a privileged account). You cannot self-authorize programmatically — on Polkadot TestNet, use the Console UI faucet to authorize your account before using PAPI to store data.


Store Your Image

## Store Your Image

Now that your account is authorized, you can store your image on-chain. Choose the method that best fits your workflow.

!!! note
    You can also interact with the Bulletin Chain directly through [Polkadot.js Apps](https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fpaseo-bulletin-rpc.polkadot.io) or [dev.papi.how](https://dev.papi.how/) by submitting extrinsics from the `transactionStorage` pallet.

=== "Console UI"

    1. Navigate to the **Upload** page in the [Bulletin Chain Console](https://paritytech.github.io/polkadot-bulletin-chain/). You can see your account's storage usage and authorization quota on the right side panel.

        ![](/images/chain-interactions/store-data/bulletin-chain/bulletin-chain-5.webp)

    2. Select the **File** tab, then drag and drop your image or click to browse. The UI shows the file name and size.

        ![](/images/chain-interactions/store-data/bulletin-chain/bulletin-chain-6.webp)

    3. Leave the **CID Configuration** at the defaults (Blake2b-256 hash, Raw codec) unless you have specific requirements.

    4. Click **Upload to Bulletin Chain** and approve the transaction in your wallet extension.

    5. On success, the UI displays your **CID**, **Block Number**, and **Transaction Index**.

        ![](/images/chain-interactions/store-data/bulletin-chain/bulletin-chain-7.webp)

    !!! warning
        Save the **Block Number** and **Transaction Index** — you'll need these values to renew your data before it expires. The Console UI auto-saves this to your browser history, but you should record it separately as well.

=== "PAPI"

    Use the [Polkadot API (PAPI)](https://papi.how/) to store an image programmatically — ideal for integrating into build scripts, CI pipelines, or dApp backends.

    **Environment Setup**

    1. Create and initialize a new project:

        ```bash
        mkdir bulletin-store-example && cd bulletin-store-example && \
        npm init -y && npm pkg set type=module
        ```

    2. Install the required dependencies:

        ```bash
        npm install polkadot-api@2.0.1 @polkadot-labs/hdkd@0.0.28 @polkadot-labs/hdkd-helpers@0.0.29 multiformats
        ```

    3. Fetch the Bulletin Chain metadata and generate typed descriptors:

        ```bash
        npx papi add bulletin -w wss://paseo-bulletin-rpc.polkadot.io
        ```

        This command connects to the Bulletin Chain RPC endpoint, downloads the chain metadata, and generates typed descriptors that provide full type safety for all pallet interactions.

    **Store an Image**

    Create a file named `store-data.ts` with the following content:

    ```typescript title="store-data.ts"
    import { createClient } from 'polkadot-api';
    import { getWsProvider } from 'polkadot-api/ws';
    import { bulletin } from '@polkadot-api/descriptors';
    import { sr25519CreateDerive } from '@polkadot-labs/hdkd';
    import {
      DEV_PHRASE,
      entropyToMiniSecret,
      mnemonicToEntropy,
    } from '@polkadot-labs/hdkd-helpers';
    import { getPolkadotSigner } from 'polkadot-api/signer';
    import { CID } from 'multiformats/cid';
    import { readFile } from 'fs/promises';

    // Bulletin Chain Polkadot TestNet RPC endpoint
    const BULLETIN_RPC = 'wss://paseo-bulletin-rpc.polkadot.io';

    // Path to the image you want to store
    const FILE_PATH = 'INSERT_IMAGE_PATH';

    async function main() (${fileData.length} bytes)`);

      // Submit the store transaction
      // Note: the signing account must have an active authorization (see Get Authorization section)
      console.log('Submitting store transaction...');
      const result = await api.tx.TransactionStorage.store({
        data: new Uint8Array(fileData),
      }).signAndSubmit(signer);

      console.log(`Transaction included in block: ${result.block.hash}`);
      console.log(`Transaction index: ${result.block.index}`);

      // Check events for the Stored event containing the CID
      for (const event of result.events)`);
          console.log(`CID: ${cid.toString()}`);
          console.log(`\nRetrieve via IPFS gateway:`);
          console.log(`https://paseo-ipfs.polkadot.io/ipfs/${cid.toString()}`);
        }
      }

      // Disconnect the client
      client.destroy();
    }

    main().catch(console.error);
    ```

    !!! note
        Replace `INSERT_IMAGE_PATH` with the path to your image (e.g., `./logo.png`). Ensure the file is under ~8 MiB. For the signer, replace the dev phrase with your own mnemonic for a Polkadot TestNet-authorized account.

    Run the script:

    ```bash
    npx tsx store-data.ts
    ```

    You should see output similar to:

        npx tsx store-data.ts
        Connected to Bulletin Chain (Polkadot TestNet)
        Read file: ./logo.png (45312 bytes)
        Submitting store transaction...
        Transaction included in block: 0x9dfc64f6...f569a658
        Transaction index: 2

        Image stored successfully!
        Index: 0
        CID: bafk2bzacea6wlxyalo6gbajlwuubv7w5dvss3vmfqmavlqy63e4vypth2ov6u

        Retrieve via IPFS gateway:
        https://ipfs.io/ipfs/bafk2bzacea6wlxyalo6gbajlwuubv7w5dvss3vmfqmavlqy63e4vypth2ov6u


Verify Your Stored Image

## Verify Your Stored Image

After storing your image, you can verify it was successfully recorded using the **Explorer** in the Console UI. Navigate to the block number from the `Stored` event to see the `TransactionStorage.Stored` event with the content hash and CID.

![](/images/chain-interactions/store-data/bulletin-chain/bulletin-chain-8.webp)


Retrieve Your Data

## Retrieve Your Data

The Bulletin Chain follows a "write-to-chain, read-from-network" architecture — you retrieve data from collator nodes using the CID. For a full overview of retrieval methods, see the [reference page](/reference/polkadot-hub/data-storage/#retrieval-methods).

=== "Console UI"

    1. Navigate to the **Download** page in the [Bulletin Chain Console](https://paritytech.github.io/polkadot-bulletin-chain/).
    2. Choose a retrieval method:

        - **P2P Connection**: Connects directly to Bulletin Chain collator nodes (decentralized, recommended).
        - **IPFS Gateway**: Uses the Bulletin Chain's IPFS gateway at `https://paseo-ipfs.polkadot.io`.

    3. Enter your **CID** in the "Fetch by CID" field — you can use either the `bafk2bzace...` format or the hex-encoded `0x0155a0e4...` format.
    4. Click **Fetch Data** to retrieve your content. The UI also generates a direct **Gateway Link** you can open in your browser.

        ![](/images/chain-interactions/store-data/bulletin-chain/bulletin-chain-9.webp)

    !!! tip
        You can also access your data directly in the browser via the Bulletin Chain IPFS gateway:

        ```text
        https://paseo-ipfs.polkadot.io/ipfs/<CID>
        ```

        For fully decentralized retrieval, use P2P or the upcoming Smoldot light client.

=== "Programmatic (Gateway)"

    You can retrieve data programmatically using the Bulletin Chain's IPFS gateway:

    ```typescript
    const cid = 'INSERT_CID';
    const response = await fetch(`https://paseo-ipfs.polkadot.io/ipfs/${cid}`);
    const data = await response.arrayBuffer();
    console.log(`Retrieved ${data.byteLength} bytes`);
    ```

    !!! note
        Replace `INSERT_CID` with the CID string you received when storing your data (e.g., `bafk2bzacea6wlxy...`).

=== "Direct P2P (Helia)"

    For production applications, the recommended decentralized approach is to connect directly to Bulletin Chain collator nodes using [Helia](https://helia.io/) (a lean IPFS implementation) and retrieve data via the CID over [libp2p](https://libp2p.io/).

    This method requires knowing the public multiaddresses of Bulletin Chain collator nodes. See the [Bulletin Chain repository](https://github.com/paritytech/polkadot-bulletin-chain) for Helia configuration examples and available collator endpoints.

!!! tip
    Smoldot light client support for retrieval via the `bitswap_block` RPC is coming soon. This will enable fully trustless, decentralized data retrieval without connecting to a full node.

!!! note
    Stored data is only available within the retention period (~2 weeks on Polkadot TestNet). After that, the data is pruned from the chain and is no longer retrievable unless it has been renewed.


Renew Your Data

## Renew Your Data

Stored data is retained for a limited period (~2 weeks on Polkadot TestNet). If you need your image to remain available beyond the retention period, renew it before it expires.

=== "Console UI"

    1. Navigate to the **Renew** page in the [Bulletin Chain Console](https://paritytech.github.io/polkadot-bulletin-chain/).
    2. Select your stored transaction from the **Load from History** dropdown, or manually enter the **Block Number** and **Transaction Index**.

        ![](/images/chain-interactions/store-data/bulletin-chain/bulletin-chain-10.webp)

    3. Click **Lookup Transaction** to view the transaction details, including the content hash, size, and expiration status.

        ![](/images/chain-interactions/store-data/bulletin-chain/bulletin-chain-11.webp)

    4. Click **Renew Storage** and approve the transaction in your wallet extension.

=== "PAPI"

    ```typescript
    console.log('Renewing stored data...');
    const result = await api.tx.TransactionStorage.renew({
      block: INSERT_BLOCK_NUMBER,
      index: INSERT_INDEX,
    }).signAndSubmit(signer);
    console.log(`Renewal included in block: ${result.block.hash}`);
    ```

    You should see output similar to:

        npx tsx renew-data.ts
        Renewing stored data...
        Renewal included in block: 0xabcd...5678

Renewal resets the retention timer, keeping your image available for another full retention period.

!!! warning
    Each renewal generates a **new block number and index**. You must track the latest `(block, index)` pair from the `Renewed` event for any subsequent renewals. Using the original values after a renewal will fail.

    !!! note
        In the future, you'll be able to reference all data by CID instead of needing to track block number and index pairs.


Where to Go Next

## Where to Go Next

-   Reference __Data Storage Reference__

    ---

    Explore the full technical reference for the Bulletin Chain, including all extrinsics, storage items, and events.

    [:octicons-arrow-right-24: Reference](/reference/polkadot-hub/data-storage/)


Introduction

## Introduction

Asset Conversion is an Automated Market Maker (AMM) utilizing [Uniswap V2](https://github.com/Uniswap/v2-core) logic and implemented as a pallet on Polkadot's Asset Hub.

This guide will provide detailed information about the key functionalities offered by the [Asset Conversion](https://github.com/paritytech/polkadot-sdk/tree/polkadot-stable2603/substrate/frame/asset-conversion) pallet on Asset Hub, including:

- Creating a liquidity pool.
- Adding liquidity to a pool.
- Swapping assets.
- Withdrawing liquidity from a pool.