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.
There are 2 types of Paymaster transactions:
defaultwhen the account is paying the fees.sponsoredwhen 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
AccountorWalletAccountV6classes - Or directly via the
PaymasterRpcclass
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:
| Name | Mainnet | Testnet |
|---|---|---|
| AVNU | https://starknet.paymaster.avnu.fi | https://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);
Sponsored Paymaster
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
},
};
- Time unit is the Starknet blockchain time unit: seconds.
executeAfteris optional. If omitted, the transaction can be executed immediately.- if
executeAfteris 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
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:
| Method | Description |
|---|---|
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.