Skip to main content
Version: 11.x

Execute calls using Paymaster

Overview​

A Paymaster in Starknet allows your account to pay gas fees using alternative tokens (e.g. ETH, USDC, ...) instead of STRK.

info

There are 2 types of Paymaster transactions:

  • default when the account is paying the fees.
  • sponsored when a dApp wants to cover the gas fees on behalf of users.

In starknet.js, you can interact with a Paymaster in two ways:

  • Through the Account or WalletAccountV6 classes
  • Or directly via the PaymasterRpc class
IMPORTANT

To be able to use the Paymaster, accounts must be compatible with SNIP-9 (Outside execution).
See SNIP-9 compatibility

Paymaster service​

Paymaster service is provided by specific backends compatible with SNIP-29.

To use a Paymaster service:

// Create a new Paymaster instance (uses a random available service)
const myPaymasterRpc = new PaymasterRpc();

If you want a specific Paymaster service:

const myPaymasterRpc = new PaymasterRpc({ nodeUrl: 'https://sepolia.paymaster.avnu.fi' });

Current available services are:

NameMainnetTestnet
AVNUhttps://starknet.paymaster.avnu.fihttps://sepolia.paymaster.avnu.fi

Account with Paymaster feature​

To instantiate a new account compatible with Paymaster:

const myAccount = new Account({
provider: myProvider,
address: accountAddress,
signer: privateKey,
paymaster: myPaymasterRpc,
});

The paymaster methods of the account come from the Paymaster plugin, installed by default. If you set the plugins option of the account yourself, see the Plugin System guide.

Getting Supported Gas Tokens​

Before sending a transaction with a Paymaster, you must first know which tokens are accepted:

const supported = await myAccount.paymaster.getSupportedTokens();
// or
const supported = await myPaymaster.getSupportedTokens();

console.log(supported);
/*
[
{
"token_address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
"decimals": 18,
"priceInStrk": "0x5ffeeacbaf058dfee0"
},
{
"token_address": "0x53b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080",
"decimals": 6,
"priceInStrk": "0x38aea"
}
]
*/

Sending a Transaction with a Paymaster​

To send a Call (result of myContract.populate() or myCallData.compile()), here for a default Paymaster transaction:

const gasToken = '0x53b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080'; // USDC in Testnet
const feesDetails: PaymasterDetails = {
feeMode: { mode: 'default', gasToken },
};
const feeEstimation = await myAccount.estimatePaymasterTransactionFee([myCall], feesDetails);
// ask here to the user to accept this fee
const res = await myAccount.executePaymasterTransaction(
[myCall],
feesDetails,
feeEstimation.suggested_max_fee_in_gas_token
);
const txR = await myProvider.waitForTransaction(res.transaction_hash);

Paymaster transaction using Contract class​

const gasToken = '0x53b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080'; // USDC in Testnet
const feesDetails: PaymasterDetails = {
feeMode: { mode: 'default', gasToken },
};
const tokenContract = new Contract({
abi: erc20Sierra.abi,
address: tokenAddress,
providerOrAccount: myAccount,
});

const feeEstimation = (await tokenContract.estimate(
'transfer',
[destinationAddress, cairo.uint256(100)],
{ paymasterDetails: feesDetails }
)) as PaymasterFeeEstimate;
// ask here to the user to accept this fee
const res1 = await tokenContract.invoke('transfer', [destinationAddress, cairo.uint256(100)], {
paymasterDetails: feesDetails,
maxFeeInGasToken: feeEstimation.suggested_max_fee_in_gas_token,
});
const txR1 = await myProvider.waitForTransaction(res1.transaction_hash);
// or
const res2 = await myTestContract
.withOptions({
paymasterDetails: feesDetails,
maxFeeInGasToken: feeEstimation.suggested_max_fee_in_gas_token,
})
.transfer(destinationAddress, cairo.uint256(100));
const txR2 = await myProvider.waitForTransaction(res2.transaction_hash);

For a sponsored transaction, use:

const myPaymasterRpc = new PaymasterRpc({
nodeUrl: 'https://sepolia.paymaster.avnu.fi',
headers: { 'x-paymaster-api-key': process.env.PAYMASTER_API_KEY },
});
const myAccount = new Account({
provider: myProvider,
address: accountAddress,
signer: privateKey,
paymaster: myPaymasterRpc,
});
const feesDetails: PaymasterDetails = {
feeMode: { mode: 'sponsored' },
};
const res = await myAccount.executePaymasterTransaction([myCall], feesDetails);
const txR = await myProvider.waitForTransaction(res.transaction_hash);

Time bounds​

Optional execution window with executeAfter and executeBefore dates:

const feesDetails: PaymasterDetails = {
feeMode: { mode: 'default', gasToken },
timeBounds: {
executeBefore: Math.floor(Date.now() / 1000) + 60 * 5, // 5 minutes
},
};
note
  • Time unit is the Starknet blockchain time unit: seconds.
  • executeAfter is optional. If omitted, the transaction can be executed immediately.
  • if executeAfter is defined, it must be strictly lower than the timestamp of the last block if you want to be able to process immediately.
  • executeBefore: the transaction is possible as long as the Unix time of the SNIP-29 server is lower than executeBefore.

Deploy Account​

important

If the account selected in the Wallet extension (Braavos, ArgentX, ...) is not deployed, you can't process a Paymaster transaction.

If necessary, deploy first the account, using:

// selectedWallet is the wallet selected by get-starknet v6.
// Get data to deploy the account:
const deploymentData = await walletV6.deploymentData(selectedWallet);
const feesDetails: PaymasterDetails = {
feeMode: { mode: 'default', gasToken },
deploymentData: { ...deploymentData, version: 1 as 1 },
};
// myWalletAccount is the WalletAccountV6 instance related to the selected wallet.
const estimatedFees: PaymasterFeeEstimate = await myWalletAccount.estimatePaymasterTransactionFee(
[],
feesDetails
);
const resp = await myWalletAccount.executePaymasterTransaction(
[],
feesDetails,
estimatedFees.suggested_max_fee_in_gas_token
);
const txR = await myWalletAccount.provider.waitForTransaction(resp.transaction_hash);

PaymasterRpc Functions​

The account.paymaster property is an instance of PaymasterRpc.

Here are the available methods:

MethodDescription
isAvailable() Returns true if the Paymaster service is up and running.
getSupportedTokens()Returns the accepted tokens and their price in STRK.
buildTransaction(...) Builds the required data (could include a typed data to sign) for the execution
executeTransaction(...)Calls the Paymaster service to execute the transaction

Examples​

Demo DAPP​

A demo DAPP is available here (needs some USDC in an account to process).

Full Example – React + Starknet.js + Paymaster​

In this example, selectedWallet is the wallet chosen by the user. The WalletAccount guide explains how to list the wallets and how to choose one.

import { FC, useEffect, useState } from 'react';
import type { WalletWithStarknetFeatures } from '@starknet-io/get-starknet-wallet-standard/features'; // v6
import { Account, PaymasterRpc, TokenData, WalletAccountV6 } from 'starknet'; // v11

const paymasterRpc = new PaymasterRpc();

const App: FC<{ selectedWallet: WalletWithStarknetFeatures }> = ({ selectedWallet }) => {
const [account, setAccount] = useState<Account>();
const [loading, setLoading] = useState(false);
const [tx, setTx] = useState<string>();
const [gasToken, setGasToken] = useState<TokenData>();
const [gasTokens, setGasTokens] = useState<TokenData[]>([]);

const handleConnect = async () => {
const walletAccount = await WalletAccountV6.connect(
{ nodeUrl: 'https://api.zan.top/public/starknet-sepolia/rpc/v0_10' },
selectedWallet,
undefined,
paymasterRpc
);
if (walletAccount.address) setAccount(walletAccount);
};

useEffect(() => {
paymasterRpc.getSupportedTokens().then((tokens) => {
setGasTokens(tokens);
});
}, []);

if (!account) {
return <button onClick={handleConnect}>Connect Wallet</button>;
}

const onClickExecute = () => {
const calls = [
{
entrypoint: 'approve',
contractAddress: '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7',
calldata: [
'0x0498E484Da80A8895c77DcaD5362aE483758050F22a92aF29A385459b0365BFE',
'0xf',
'0x0',
],
},
];
setLoading(true);
account
.executePaymasterTransaction(calls, {
feeMode: { mode: 'default', gasToken: gasToken!.token_address },
})
.then((res) => {
setTx(res.transaction_hash);
setLoading(false);
})
.catch((err) => {
console.error(err);
setLoading(false);
});
};

return (
<div>
<div>
<p>
<strong>Gas tokens</strong>
</p>
{gasTokens.map((token) => (
<button
key={token.token_address}
disabled={token.token_address === gasToken?.token_address}
onClick={() => setGasToken(token)}
>
{token.token_address}
</button>
))}
</div>
{tx && (
<a href={`https://sepolia.voyager.online/tx/${tx}`} target={'_blank'} rel="noreferrer">
Success:{tx}
</a>
)}
{!gasToken && <p>Select a gas token</p>}
<div>
{account && (
<button disabled={loading || !gasToken} onClick={onClickExecute}>
{loading ? 'Loading' : 'Execute'}
</button>
)}
</div>
</div>
);
};

export default App;

For more information about defining call messages and parameters, see this guide.