This guide demonstrates how to implement gas sponsorship on Cosmos networks using fee grants. Unlike EVM chains that use account abstraction for gasless transactions, Cosmos networks natively support gas sponsorship through fee grants, allowing a grantor address to pay for another account’s transaction fees.
Prerequisites
Understanding Fee Grants
Fee grants in Cosmos allow one account (grantor) to pay transaction fees for another account (grantee). This mechanism provides native gas sponsorship without requiring smart contracts or account abstraction.
Key concepts:
- Grantor: The account that pays for gas fees
- Grantee: The account whose transactions are sponsored
- Allowance: Defines spending limits and expiration for the grant
Only one fee grant is allowed per granter-grantee pair. Self-grants are not permitted.
Creating a Basic Fee Grant
Create a basic allowance to grant gas sponsorship to another address:
import { MsgGrantAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/tx";
import { BasicAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/feegrant";
const granterAddress = paraSigner.address; // From your Para signer setup
const granteeAddress = "cosmos1grantee..."; // Replace with actual grantee address
// Create basic allowance with spending limits
const basicAllowance = BasicAllowance.fromPartial({
spendLimit: [{
denom: "uatom",
amount: "1000000" // 1 ATOM limit
}],
expiration: {
seconds: BigInt(Math.floor(Date.now() / 1000) + 86400), // 24 hours from now
nanos: 0
}
});
// Create the grant message
const grantMsg = {
typeUrl: "/cosmos.feegrant.v1beta1.MsgGrantAllowance",
value: MsgGrantAllowance.fromPartial({
granter: granterAddress,
grantee: granteeAddress,
allowance: {
typeUrl: "/cosmos.feegrant.v1beta1.BasicAllowance",
value: BasicAllowance.encode(basicAllowance).finish()
}
})
};
// Sign and broadcast the grant transaction
const result = await client.signAndBroadcast(
granterAddress,
[grantMsg],
"auto", // Let the client estimate gas
"Granting fee allowance"
);
Using Fee Grants as a Grantee
Once a fee grant is established, the grantee can perform transactions with sponsored gas fees:
// Create a signer for the grantee
const granteeSigner = new ParaProtoSigner(granteeParaInstance, "cosmos");
const granteeClient = await SigningStargateClient.connectWithSigner(rpcUrl, granteeSigner);
// Send tokens with sponsored gas
const result = await granteeClient.sendTokens(
granteeAddress,
"cosmos1recipient...",
[{ denom: "uatom", amount: "100000" }], // 0.1 ATOM
{
amount: [{ denom: "uatom", amount: "5000" }],
gas: "200000",
granter: granterAddress // This tells the network to use the fee grant
}
);
Querying Fee Grants
Check existing grants before creating new ones:
// Query grants for a specific grantee
const grantsByGrantee = await fetch(
`${restUrl}/cosmos/feegrant/v1beta1/allowances/${granteeAddress}`
).then(res => res.json());
// Query all grants by a specific granter
const grantsByGranter = await fetch(
`${restUrl}/cosmos/feegrant/v1beta1/issued/${granterAddress}`
).then(res => res.json());
// Query a specific grant
const specificGrant = await fetch(
`${restUrl}/cosmos/feegrant/v1beta1/allowance/${granterAddress}/${granteeAddress}`
).then(res => res.json());
Other Allowance Types
Periodic Allowance
Resets spending limits periodically:
import { PeriodicAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/feegrant";
const periodicAllowance = PeriodicAllowance.fromPartial({
basic: {
spendLimit: [{
denom: "uatom",
amount: "10000000" // 10 ATOM total limit
}],
expiration: null // No expiration
},
period: { seconds: BigInt(86400), nanos: 0 }, // 24 hours
periodSpendLimit: [{
denom: "uatom",
amount: "1000000" // 1 ATOM per period
}]
});
Allowed Message Allowance
Restricts which message types can be sponsored:
import { AllowedMsgAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/feegrant";
const allowedMsgAllowance = AllowedMsgAllowance.fromPartial({
allowance: {
typeUrl: "/cosmos.feegrant.v1beta1.BasicAllowance",
value: BasicAllowance.encode(basicAllowance).finish()
},
allowedMessages: [
"/cosmos.bank.v1beta1.MsgSend",
"/cosmos.staking.v1beta1.MsgDelegate"
]
});
Revoking Fee Grants
Remove a fee grant when it’s no longer needed:
import { MsgRevokeAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/tx";
const revokeMsg = {
typeUrl: "/cosmos.feegrant.v1beta1.MsgRevokeAllowance",
value: MsgRevokeAllowance.fromPartial({
granter: granterAddress,
grantee: granteeAddress
})
};
const result = await client.signAndBroadcast(
granterAddress,
[revokeMsg],
"auto"
);
For production applications, use server-side controlled wallets as grantors while allowing users to authenticate client-side as grantees. This pattern uses Para’s pregenerated wallets to create an app-controlled grantor wallet.
Wallet Pregeneration Guide
Server-Side Setup
Create and manage a grantor wallet on your server:
// server.ts
import { Para } from "@getpara/server-sdk@alpha";
import { SigningStargateClient } from "@cosmjs/stargate";
import { ParaProtoSigner } from "@getpara/cosmjs-adapter@alpha";
const serverPara = new Para(process.env.PARA_API_KEY);
// Create a pregen wallet for gas sponsorship
const grantorWallet = await serverPara.createPregenWallet({
type: 'COSMOS',
pregenId: { customId: "app-gas-sponsor-wallet" }
});
// Store the user share securely
const userShare = await serverPara.getUserShare();
// Store userShare in your secure database
API Endpoint for Creating Grants
// POST /api/create-fee-grant
async function createFeeGrantForUser(userAddress: string) {
// Load the grantor wallet
await serverPara.setUserShare(storedUserShare);
// Set up Cosmos client
const granterSigner = new ParaProtoSigner(serverPara, "cosmos");
const client = await SigningStargateClient.connectWithSigner(rpcUrl, granterSigner);
// Check if grant already exists
const existingGrant = await fetch(
`${restUrl}/cosmos/feegrant/v1beta1/allowance/${granterSigner.address}/${userAddress}`
).then(res => res.json());
if (existingGrant.allowance) {
// Revoke existing grant first
const revokeMsg = {
typeUrl: "/cosmos.feegrant.v1beta1.MsgRevokeAllowance",
value: MsgRevokeAllowance.fromPartial({
granter: granterSigner.address,
grantee: userAddress
})
};
await client.signAndBroadcast(
granterSigner.address,
[revokeMsg],
"auto"
);
}
// Create new grant with daily limit
const basicAllowance = BasicAllowance.fromPartial({
spendLimit: [{
denom: "uatom",
amount: "1000000" // 1 ATOM daily limit
}],
expiration: {
seconds: BigInt(Math.floor(Date.now() / 1000) + 86400),
nanos: 0
}
});
const grantMsg = {
typeUrl: "/cosmos.feegrant.v1beta1.MsgGrantAllowance",
value: MsgGrantAllowance.fromPartial({
granter: granterSigner.address,
grantee: userAddress,
allowance: {
typeUrl: "/cosmos.feegrant.v1beta1.BasicAllowance",
value: BasicAllowance.encode(basicAllowance).finish()
}
})
};
const result = await client.signAndBroadcast(
granterSigner.address,
[grantMsg],
"auto"
);
return {
transactionHash: result.transactionHash,
granterAddress: granterSigner.address
};
}
Client-Side Integration
// client.tsx
import { useParaWallet } from "@getpara/react-sdk@alpha";
import { SigningStargateClient } from "@cosmjs/stargate";
import { ParaProtoSigner } from "@getpara/cosmjs-adapter@alpha";
function MyApp() {
const { para, address } = useParaWallet();
const [granterAddress, setGranterAddress] = useState<string>();
const setupGasSponsorship = async () => {
if (!address) return;
const response = await fetch('/api/create-fee-grant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userAddress: address })
});
const { granterAddress } = await response.json();
setGranterAddress(granterAddress);
};
const performSponsoredTransaction = async (recipientAddress: string, amount: string) => {
if (!para || !address || !granterAddress) return;
const signer = new ParaProtoSigner(para, "cosmos");
const client = await SigningStargateClient.connectWithSigner(rpcUrl, signer);
const result = await client.sendTokens(
address,
recipientAddress,
[{ denom: "uatom", amount }],
{
amount: [{ denom: "uatom", amount: "5000" }],
gas: "200000",
granter: granterAddress // App pays the gas fees
}
);
return result.transactionHash;
};
return (
<>
<button onClick={setupGasSponsorship}>
Activate Gas Sponsorship
</button>
<button onClick={() => performSponsoredTransaction("cosmos1...", "100000")}>
Send Sponsored Transaction
</button>
</>
);
}
Complete Server Implementation
// Complete server implementation with grant management
class FeeGrantService {
private serverPara: Para;
private granterAddress?: string;
constructor() {
this.serverPara = new Para(process.env.PARA_API_KEY);
}
async initialize() {
// Load or create grantor wallet
const userShare = await loadUserShareFromDatabase();
await this.serverPara.setUserShare(userShare);
const signer = new ParaProtoSigner(this.serverPara, "cosmos");
this.granterAddress = signer.address;
}
async createGrant(userAddress: string, limitAmount: string, periodSeconds: number) {
const signer = new ParaProtoSigner(this.serverPara, "cosmos");
const client = await SigningStargateClient.connectWithSigner(rpcUrl, signer);
const allowance = BasicAllowance.fromPartial({
spendLimit: [{
denom: "uatom",
amount: limitAmount
}],
expiration: {
seconds: BigInt(Math.floor(Date.now() / 1000) + periodSeconds),
nanos: 0
}
});
const msg = {
typeUrl: "/cosmos.feegrant.v1beta1.MsgGrantAllowance",
value: MsgGrantAllowance.fromPartial({
granter: this.granterAddress,
grantee: userAddress,
allowance: {
typeUrl: "/cosmos.feegrant.v1beta1.BasicAllowance",
value: BasicAllowance.encode(allowance).finish()
}
})
};
return client.signAndBroadcast(this.granterAddress, [msg], "auto");
}
async revokeGrant(userAddress: string) {
const signer = new ParaProtoSigner(this.serverPara, "cosmos");
const client = await SigningStargateClient.connectWithSigner(rpcUrl, signer);
const msg = {
typeUrl: "/cosmos.feegrant.v1beta1.MsgRevokeAllowance",
value: MsgRevokeAllowance.fromPartial({
granter: this.granterAddress,
grantee: userAddress
})
};
return client.signAndBroadcast(this.granterAddress, [msg], "auto");
}
}
Best Practices
- Set appropriate spending limits based on expected transaction volume
- Use expiration times to automatically clean up unused grants
- Monitor grant usage to control costs and detect abuse
- Consider using periodic allowances for regular users
- Use allowed message allowances to restrict transaction types
- Remember that creating and revoking grants also incur gas costs
Fee grants provide native gas sponsorship on Cosmos networks without requiring smart contracts, making them more efficient than EVM account abstraction solutions.