When a Polkadot/Substrate extrinsic fails at the RPC layer, the JSON-RPC error '1010: Invalid Transaction' is a wrapper whose data field contains a transaction-pool validity tag (e.g., Payment, Stale, TemporarilyBanned) that reveals the true cause. For extrinsics that pass the pool but fail during execution, the failure surfaces via author_submitAndWatchExtrinsic events and a runtime DispatchError, which you decode using the chain's metadata. This article explains the mechanism, provides a runnable @polkadot/api script to capture and decode these errors, and gives a tag-to-fix checklist.
Direct Answer: What 1010 Invalid Transaction Really Means
When you submit an extrinsic to a Polkadot or Substrate node via author_submitExtrinsic or author_submitAndWatchExtrinsic, the node's transaction pool performs a validity check before accepting the transaction. If that check fails, the RPC returns a JSON-RPC error with code 1010 and message Invalid Transaction. The actual reason is encoded in the data field as a tag such as Payment, Stale, or TemporarilyBanned. This tag is not a random string; it corresponds to the InvalidTransaction enum variants defined in the Substrate transaction-pool framework. To fix your submission, you must decode that tag and address the underlying issue—whether it's insufficient funds, a bad nonce, or a banned sender. For extrinsics that pass the pool but fail during execution, the failure appears later as a DispatchError in the transaction events, which you decode using the chain's runtime metadata.
This guide is part of the OnFinality Learn hub and focuses on the Polkadot ecosystem. If you're new to Polkadot RPC endpoints, see the Polkadot RPC guide first. For transport-level issues like timeouts or rate limits, refer to our timeout, latency, and rate limit articles.
How Extrinsic Submission Works Under the Hood
Submitting an extrinsic to a Polkadot node is a two-stage process. First, the transaction pool validates the extrinsic against the current state and the pool's rules. This is where the 1010 Invalid Transaction error originates. The pool checks things like the nonce (whether it's the next expected one), the transaction's longevity (whether it's not too old), and the sender's ability to pay fees. If any check fails, the pool returns an InvalidTransaction variant, which the RPC layer serializes into the error's data field.
Second, if the extrinsic passes the pool, it is propagated and included in a block. Execution happens during block production. If the extrinsic's call fails at runtime—for example, because of a bad origin or a pallet-specific error—the transaction is not reverted; instead, it is included in the block but marked as failed. The failure is reported through the system.ExtrinsicFailed event, which contains a DispatchError. To see this, you must use author_submitAndWatchExtrinsic, which emits transactionStatus updates including Invalid and Drop for pool rejections, and Finalized with the block hash for successful inclusion. The DispatchError is not part of the RPC error; you must query the block events to decode it.
This mechanism is documented in the Substrate transaction-pool documentation and the FRAME dispatch documentation. The Polkadot developer docs also cover extrinsics and transactions.
Decoding the 1010 Error Data Tag
The data field of a 1010 Invalid Transaction error is a string that matches one of the InvalidTransaction variants from the Substrate transaction-pool. The most common ones you'll encounter are:
Payment– The sender cannot pay the transaction fee (e.g., insufficient balance or a corrupted fee calculation).
Stale– The nonce is too low (already used) or the transaction is too old.
Future– The nonce is higher than the current account nonce (not yet valid).
TemporarilyBanned– The sender is temporarily banned from the pool, often due to too many invalid submissions.
BadProof– The signature or signed payload is invalid.
AncientBirthBlock– The transaction'sera(mortality) is too old; the birth block is beyond theBlockHashCount.
ExhaustsResources– The pool is full or the transaction would exceed block weight limits.
Custom(u8)– A chain-specific validity error, often from a custom transaction extension.
To see the exact tag, you must catch the error object in your client code. The tag is in error.data (or error.data.toString() in some libraries). Do not rely on the message alone, as it is generic.
The following table maps each tag to its typical cause and fix. This is based on the Substrate source code and community experience.
- Payment – Cause: insufficient balance for fees or a fee-related issue. Fix: ensure the account has enough free balance to cover the fee plus any existential deposit; check the fee via
api.tx.balances.transfer.estimate. - Stale – Cause: nonce too low or transaction too old. Fix: use the current nonce from
api.query.system.accountand set a properera(e.g.,api.tx.balances.transferwithera: 64). - Future – Cause: nonce too high. Fix: wait for previous transactions to be processed or set the correct nonce.
- TemporarilyBanned – Cause: repeated invalid submissions from the same sender. Fix: wait for the ban to expire (usually a few minutes) and fix the underlying issue.
- BadProof – Cause: invalid signature or signed payload. Fix: ensure you sign with the correct account and that the payload matches the chain's signed extensions.
- AncientBirthBlock – Cause: the transaction's
erais too long or the birth block is too old. Fix: use a shortereraor let the API set it automatically. - ExhaustsResources – Cause: pool is full or the transaction would exceed block limits. Fix: retry later or reduce the transaction's complexity.
- Custom(u8) – Cause: chain-specific validity error. Fix: consult the chain's documentation or source for the meaning of the custom code.
Runnable Example: Capture and Decode 1010 Errors
The following Node.js script uses @polkadot/api to connect to a user-supplied WebSocket endpoint, build and sign a simple transfer extrinsic, and submit it. It catches the JSON-RPC error and prints the full error object, including the data field. It also demonstrates how to use author_submitAndWatchExtrinsic to capture execution events if the transaction is accepted.
Prerequisites: Node.js 18+, @polkadot/api version 10.9.1 (as of 2026-09-05). Install with npm install @polkadot/api. Replace WS_URL with your endpoint (e.g., wss://rpc.polkadot.io for Polkadot, wss://statemint-rpc.polkadot.io for Asset Hub).
Important: The script builds a transfer of 0 DOT to a dummy address. This is safe because the amount is zero, but it may still fail if the sender has no funds to pay fees. To avoid spending funds, you can use a read-only call like api.tx.balances.transfer with a zero amount, but note that some chains reject zero-value transfers. For a safe demonstration, you can also use api.tx.system.remark with a small remark, which only requires fees. The script is designed to surface errors, not to execute a real transfer.
const { ApiPromise, WsProvider, Keyring } = require('@polkadot/api');
// Replace with your endpoint
const WS_URL = 'wss://rpc.polkadot.io';
async function main() {
const provider = new WsProvider(WS_URL);
const api = await ApiPromise.create({ provider });
// Create a keyring from a dev seed (for testing only)
const keyring = new Keyring({ type: 'sr25519' });
const alice = keyring.addFromUri('//Alice');
// Build a transfer of 0 DOT to a dummy address
const dummy = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'; // Alice's address for demo
const tx = api.tx.balances.transfer(dummy, 0);
// Sign the transaction
const signed = await tx.signAsync(alice);
// Submit and watch
try {
const unsub = await signed.send(({ status, events, dispatchError }) => {
if (status.isInBlock || status.isFinalized) {
console.log('Transaction included in block:', status.asInBlock.toHex());
if (dispatchError) {
console.log('Dispatch error:', decodeDispatchError(api, dispatchError));
}
events.forEach(({ event }) => {
if (api.events.system.ExtrinsicFailed.is(event)) {
console.log('Extrinsic failed:', event.data.toString());
}
});
unsub();
}
});
} catch (error) {
// This is where the 1010 error appears
console.error('Submission error:', JSON.stringify(error, null, 2));
if (error.data) {
console.log('Error data (tag):', error.data.toString());
}
}
await api.disconnect();
}
function decodeDispatchError(api, dispatchError) {
if (dispatchError.isModule) {
const { index, error } = dispatchError.asModule;
const meta = api.registry.findMetaError({ index, error });
return `${meta.section}.${meta.name}: ${meta.docs.join(' ')}`;
} else {
return dispatchError.toString();
}
}
main().catch(console.error);Expected Output and Results Table
When you run the script with an account that has no funds, you will likely see an error like this (actual output varies by chain and account state):
If the transaction is accepted, you'll see a block hash and possibly a dispatch error. Fill in the table below with your own results to document the behavior on your target chain.
- Chain/Endpoint: e.g., Polkadot, Asset Hub, or a custom parachain.
- Account balance: The free balance of the signing account.
- Nonce used: The nonce you set (or auto-filled).
- Error code: e.g., 1010 or 0.
- Error data tag: e.g., Payment, Stale, etc.
- Dispatch error (if any): e.g.,
Module { index: 5, error: 3 }decoded tobalances.InsufficientBalance. - Fix applied: What you changed to resolve the issue.
{
"code": 1010,
"message": "Invalid Transaction",
"data": "Payment"
}
Decoding Dispatch Errors from Execution Failures
When an extrinsic passes the pool but fails during execution, the failure is not returned as an RPC error. Instead, the transaction is included in a block, and the system.ExtrinsicFailed event is emitted. The event contains a DispatchError which can be one of several variants:
Module { index, error }– A pallet-specific error. Theindexrefers to the pallet's index in the runtime, anderroris the error index within that pallet. You must decode these using the runtime metadata.
BadOrigin– The origin (sender) is not allowed to call this function.
Token– A token error such asNoFundsorBelowMinimum.
Arithmetic– An arithmetic overflow or underflow.
Other– A catch-all for other errors.
To decode a Module error, you need the runtime metadata. The @polkadot/api provides a helper: api.registry.findMetaError({ index, error }). This returns an object with section, name, and docs. For example, if you get Module { index: 5, error: 3 }, it might decode to balances.InsufficientBalance.
The script above includes a decodeDispatchError function that does this automatically. Note that the pallet index can vary between chains, so always use the metadata from the specific chain you're querying.
For more on dispatch errors, see the FRAME dispatch documentation and the Polkadot developer docs on transactions.
Common Mistakes and How to Avoid Them
Many extrinsic submission failures stem from a few recurring mistakes. Here's a checklist to diagnose them:
- Incorrect nonce: If you're submitting multiple transactions from the same account, you must increment the nonce manually or use
api.derive.balances.accountto get the current nonce. Using the same nonce twice will cause aStaleerror.
- Insufficient funds for fees: Even if the transfer amount is zero, you need enough balance to cover the transaction fee. Check the fee with
api.tx.balances.transfer.estimatebefore submitting.
- Wrong era (mortality): If you set a custom
erathat is too long, the transaction may be rejected asAncientBirthBlock. Useapi.tx.balances.transferwithout specifying an era to let the API set a safe default.
- Using a stale endpoint: If you're connected to a node that is behind, your transaction may be rejected as
Stale. Ensure you're connected to a synced node. OnFinality provides reliable endpoints; see our Polkadot network page for details.
- Not handling the
datafield: Many developers only check the error message and miss the tag. Always log the full error object.
- Assuming all chains are the same: Pallet indices and error codes vary between Polkadot and parachains. Always use the metadata from the specific chain.
For transport-level issues like timeouts or rate limits, see our WebSocket guide and rate limits article.
Limitations and Tradeoffs
The methods described here rely on the node's transaction pool and runtime metadata. There are some limitations:
- Node-specific behavior: The transaction pool's validity checks can vary slightly between node versions and chain configurations. The
1010error is standard, but the exact tags may differ on custom parachains.
- Dispatch errors are only visible after inclusion: If you use
author_submitExtrinsic(not watch), you won't see execution failures. You must useauthor_submitAndWatchExtrinsicand listen for events.
- Metadata changes: Runtime upgrades can change pallet indices and error codes. Always fetch the latest metadata from the chain.
- Provider differences: Some RPC providers may wrap errors differently or add extra fields. OnFinality's endpoints follow the standard Substrate RPC, but if you use a third-party provider, test the error format. For pricing and service details, see our RPC pricing and API service pages.
This guide is not a substitute for reading the chain's documentation. For Polkadot-specific details, refer to the official Polkadot docs.
Next Steps and Further Reading
Now that you can decode 1010 errors and dispatch failures, you can debug your extrinsics more effectively. To go further:
- Explore the Polkadot RPC guide for a complete list of RPC methods.
- Learn about Polkadot RPC timeouts and latency to optimize your connection.
- Understand rate limits and 429s to avoid being throttled.
- For WebSocket-specific issues, see the Polkadot WebSocket RPC guide.
- If you're building on a parachain, check the chain's own documentation for custom transaction extensions and errors.
If you need a reliable RPC endpoint, OnFinality offers public and private endpoints; see our network page for details. For production use, consider our API service for dedicated support.