Meta Transactions Support for Polkadot
Meta Transactions are coming to Polkadot and are now released on Westend for testing. The feature is ready on Westend’s Asset Hub, Collectives, Coretime, and People chains, and we’d like to share how to use it.
Introduction
The concept of Meta Transaction is well-established in the Ethereum ecosystem, referring to a transaction authorised by one party (signer) and executed by an untrusted third party that covers the transaction fees (relayer). This concept proves useful in scenarios where the signer lacks the assets to cover the fee or lacks the incentive to do so.
Examples include:
- dApp covering transaction fees for its users
- proxy accounts lacking balance
- transaction fees paid in any asset when a signer incentivizes a relayer to cover the fee, as demonstrated by a transaction such as
batch([sendCustomToken(relayerAddr, amount), doTheActualWork()]) - signer delegating voting power without covering the transaction fee.
For the implementation details refer to the RFC polkadot-sdk/4123.
How It Works
Much like a regular transaction, the signer part of a meta transaction is constructed from the call the signer wishes to execute, the extension version, and a set of extensions, including one that carries the signer’s signature. The extensions concept is inherited from regular transactions, so many will already be familiar: CheckMortality, CheckGenesis, CheckNonce, and others. These extensions serve the same purpose here as they do in regular transactions, enforcing constraints such as expiration, chain-specific validity, and protection against double-spend attacks. One additional extension, MetaTxMarker, ensures that meta transactions cannot be submitted as regular transactions.
Once the signer has assembled their part of the meta transaction, rather than publishing it on-chain directly, they share it with any interested relayer. The relayer then takes the signer’s payload as an argument to the dedicated call meta_tx.dispatch(meta_tx, meta_tx_encoded_len), wraps it in a regular transaction, and submits it on-chain. Transaction fees are charged to the relayer, while the enclosed call is executed on behalf of the original signer.
/// Meta Transaction type.
///
/// The data that is provided and signed by the signer and shared with the relayer.
pub struct MetaTx<Call, Extension> {
/// The target call to be executed on behalf of the signer.
call: Call,
/// The extension version.
extension_version: ExtensionVersion,
/// The extension/s for the meta transaction.
extension: Extension, // example: (CheckMortality, CheckNonce, CheckGenesis, VerifySignature)
}
/// Processes and executes a meta-transaction on behalf of its signer.
fn meta-tx.dispatch(
meta_tx: MetaTx,
meta_tx_encoded_len: u32,
);
Runtime Setup
If you want your runtime to support meta transactions you’ll need to setup a pallet where you need to think only about one config parameter, which is Extensions. Those are the extensions that the signer has to include into its part of the meta transaction.
Here is the example:
pub type MetaTxExtension = (
// Verify Signature is used as an extension to ensure signature is valid
pallet_verify_signature::VerifySignature<Runtime>,
// Ensure that this will only execute as meta transaction, not as regular
pallet_meta_tx::MetaTxMarker<Runtime>,
frame_system::CheckNonZeroSender<Runtime>,
frame_system::CheckSpecVersion<Runtime>,
frame_system::CheckTxVersion<Runtime>,
// Ensures the call is executed on the right chain
frame_system::CheckGenesis<Runtime>,
// Enables the expiration on signature validity
frame_system::CheckMortality<Runtime>,
// Ensures there is no double spending
frame_system::CheckNonce<Runtime>,
frame_metadata_hash_extension::CheckMetadataHash<Runtime>
);
Integration into dApp
Following code & explanation would help understand how to build meta transactions & integrate it into custom dApps. Note that this is implemented using PAPI (Polkadot API) with Bun runtime & for custom packages needs adaptation.
Scenario given in the code: Alice is signer & Bob is relayer. Alice builds a call of system remark event “i am a signer” and signs it with extension data, and Bob publishes on chain it through Meta Tx pallet.
Meta Tx Request
Check the full code here if you need more context.
1. Build the Call Data
Create the remark call using PAPI’s typed API and extract its encoded bytes:
const remark = stringToU8a("i am a signer");
const call = api.tx.System.remark_with_event({
remark: Binary.fromBytes(remark),
});
const encodedCallData = (await call.getEncodedData()).asBytes();
2. Providing Data Needed to Build Extensions
Retrieve the runtime version, genesis hash, and Alice’s nonce — these bind the signature to a specific chain and runtime:
const genesisHashHex = await client._request<string>(
"chain_getBlockHash",
[0]
);
const genesisHash = hexToU8a(genesisHashHex);
const rv = await client._request<{
specVersion: number;
transactionVersion: number;
}>("state_getRuntimeVersion", []);
const aliceAddress = ss58Address(alice.publicKey);
const accountInfo = await api.query.System.Account.getValue(aliceAddress);
const nonce = Number(accountInfo.nonce);
3. Encode Extension Data
Explicit and implicit extension data:
const extensionExplicit = u8aConcat(
new Uint8Array([0x00]), // Era::Immortal
compactToU8a(nonce), // nonce (compact encoded)
new Uint8Array([0x00]) // metadata hash mode: disabled
);
const extensionImplicit = u8aConcat(
stringToU8a("_meta_tx"), // MetaTxMarker domain separator
u32ToLeBytes(rv.specVersion), // spec version
u32ToLeBytes(rv.transactionVersion), // tx version
genesisHash, // genesis hash
genesisHash, // block hash (immortal → genesis)
new Uint8Array([0x00]) // metadata hash: None
);
4. Collect Data for Signature and Sign
Concatenate everything, hash with blake2-256, and sign:
const extensionDataToBeSigned = u8aConcat(
new Uint8Array([0x00]), // extension_version = 0
encodedCallData,
extensionExplicit,
extensionImplicit
);
const hash = blake2AsU8a(extensionDataToBeSigned, 256);
const signature = alice.sign(hash);
5. Build Final Meta Tx Request from Signer
// assemble explicit extensions
const metaTxExtension = [
Enum("Signed", { // VerifySignature
signature: Enum("Sr25519", FixedSizeBinary.fromBytes(signature)),
account: aliceAddress,
}),
undefined, // MetaTxMarker
undefined, // CheckNonZeroSender
undefined, // CheckSpecVersion
undefined, // CheckTxVersion
undefined, // CheckGenesis
Enum("Immortal"), // CheckMortality
nonce, // CheckNonce
Enum("Disabled"), // CheckMetadataHash
];
const metaTx = {
call: call,
extension_version: 0,
extension: metaTxExtension,
};
const codecs = await getTypedCodecs(localdev);
// Encode Meta Tx and share with the world
const metaTxEncoded = u8aToHex(codecs.tx.MetaTx.dispatch.inner.meta_tx.enc(metaTx));
Relay Transaction
1. Build the MetaTx.dispatch Call
Wrap the signer’s meta transaction into a MetaTx.dispatch extrinsic. Use getTypedCodecs to encode just the meta_tx field and measure its length:
const codecs = await getTypedCodecs(localdev);
const decodedMetaTx = codecs.tx.MetaTx.dispatch.inner.meta_tx.dec(hexToU8a(metaTxEncoded));
const metaTxEncodedLen = hexToU8a(metaTxEncoded).length;
const dispatchCall = api.tx.MetaTx.dispatch({
meta_tx: decodedMetaTx,
meta_tx_encoded_len: metaTxEncodedLen,
});
2. Sign and Submit
Bob signs the outer transaction with his own key and submits it. Bob pays the transaction fees:
const result = await dispatchCall.signAndSubmit(bobSigner);
console.log("Finalized in block:", result.block.hash);
console.log("Block number:", result.block.number);
console.log("Success:", result.ok);

