Send a Transaction While Paying the Fee with a Different Token¶
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), Polkadot.js API, or Subxt. Select your preferred SDK in the code tabs below.
Polkadot.js API Maintenance Mode
The Polkadot.js API is no longer actively developed. New projects should use Polkadot-API (PAPI) or Dedot as actively maintained alternatives.
Prerequisites¶
Before starting, ensure you have the following installed:
- Chopsticks — to fork Polkadot Hub locally
- Your preferred SDK:
- Polkadot-API (PAPI) (TypeScript)
- Polkadot.js API (JavaScript)
- Subxt (Rust)
Local Polkadot Hub Setup¶
Fork the Polkadot Hub locally using Chopsticks:
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 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¶
-
Create a new directory and initialize the project:
-
Initialize the project:
-
Install dev dependencies:
-
Install dependencies:
-
Create TypeScript configuration:
-
Generate Polkadot API types for Polkadot Hub:
-
Create a new file called
fee-payment-transaction.ts:
-
Create a new directory and initialize the project:
-
Initialize the project:
-
Install dependencies:
-
Create a new file called
fee-payment-transaction.js:
-
Create a new Rust project:
-
Install the Subxt CLI to download chain metadata:
-
Download Polkadot Hub metadata from the local Chopsticks fork:
Note
Ensure your Chopsticks fork is running at
ws://localhost:8000before downloading the metadata. -
Update
Cargo.tomlwith the required dependencies: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"] } -
Create the source file:
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:
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;
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;
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:
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 using Alice's development account and connect to the local Polkadot Hub:
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),
The cryptoWaitReady() call ensures the underlying WASM cryptographic libraries are initialized before creating the keyring:
async function main() {
// Wait for crypto libraries to be ready
await cryptoWaitReady();
// Create a keyring instance and add Alice's account
const keyring = new Keyring({ type: 'sr25519' });
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)');
Notice that the OnlineClient is parameterized with AssetHubConfig instead of the default PolkadotConfig:
#[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 a standard DOT transfer transaction that sends 3 DOT to Bob's address while keeping Alice's account alive:
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:
In PAPI, you specify the alternative asset through the asset parameter in the signAndSubmit options:
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) {
const type = (ev as any).type ?? "unknown";
console.log(`- ${type}`);
}
process.exit(0);
In Polkadot.js, you define the asset as an XCM multi-location object and pass it as the assetId option to signAndSend:
// 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(
`\nTransaction finalized in block: ${status.asFinalized.toHex()}`
);
console.log(`Transaction hash: ${txHash.toHex()}`);
// Display all events
console.log('\nEvents:');
events.forEach(({ event }) => {
console.log(` ${event.section}.${event.method}`);
});
if (unsubscribe) {
unsubscribe();
}
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(
dispatchError.asModule
);
const { docs, name, section } = decoded;
reject(new Error(`${section}.${name}: ${docs.join(' ')}`));
} else {
reject(new Error(dispatchError.toString()));
}
} else {
resolve();
}
}
}
)
.then((unsub) => {
unsubscribe = unsub;
})
.catch((error) => {
if (unsubscribe) {
unsubscribe();
}
reject(error);
});
});
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:
// 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() {
let event = event?;
println!(
" {}.{}",
event.pallet_name(),
event.event_name()
);
}
Full Code¶
Complete Code
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) {
const type = (ev as any).type ?? "unknown";
console.log(`- ${type}`);
}
process.exit(0);
Complete Code
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() {
// Wait for crypto libraries to be ready
await cryptoWaitReady();
// Create a keyring instance and add Alice's account
const keyring = new Keyring({ type: 'sr25519' });
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(
`\nTransaction finalized in block: ${status.asFinalized.toHex()}`
);
console.log(`Transaction hash: ${txHash.toHex()}`);
// Display all events
console.log('\nEvents:');
events.forEach(({ event }) => {
console.log(` ${event.section}.${event.method}`);
});
if (unsubscribe) {
unsubscribe();
}
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(
dispatchError.asModule
);
const { docs, name, section } = decoded;
reject(new Error(`${section}.${name}: ${docs.join(' ')}`));
} else {
reject(new Error(dispatchError.toString()));
}
} else {
resolve();
}
}
}
)
.then((unsub) => {
unsubscribe = unsub;
})
.catch((error) => {
if (unsubscribe) {
unsubscribe();
}
reject(error);
});
});
// Disconnect from the node
await api.disconnect();
}
main().catch(console.error);
Complete Code
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() {
let event = event?;
println!(
" {}.{}",
event.pallet_name(),
event.event_name()
);
}
Ok(())
}
Run the Script¶
Expected Output¶
When you run the script successfully, you should see output similar to:
Tx finalized: 0xfe4e3fa64d374e256c72463c507743f16672caaf1b4e539fe913026de394009e (ok=true) Block: #12255461 0xaf315c306304ad175e4e24c5c8cbf97518c1411183bbf81a6107209a49d84f4d [tx index 2] Events: - Assets - Balances - Assets - AssetConversion - Balances - System - Balances - Balances - Balances - AssetTxPayment - System
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
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¶
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¶
-
Guide Send Transactions with SDKs
Learn how to send various types of transactions using different SDKs.
-
Guide Calculate Transaction Fees
Understand how to estimate transaction fees before submitting.
| Created: October 3, 2025