-
Two permissions, added with the ordinary
AddKey:GasKeyFullAccess { balance, num_nonces }, orGasKeyFunctionCall { balance, num_nonces, receiver_id, method_names }scoped to one contract like a function-call key. -
Balance starts at 0.
AddKeycannot set one; fund it afterwards. A function-call gas key has noallowance(the view showsallowance: null): the balance is the allowance. -
Lanes.
num_nonces(1 to 1,024) independent nonce sequences. A send names its lane withnonceIndex; lanes never wait on each other, and one lane is sequential. TheAddKeyfee grows with the count. -
Strings out.
balanceis yoctoNEAR as a decimal string.near.gasKeyInfoFromPermissionturns aview_access_keypermission into{ balance, num_nonces, functionCall }, ornullfor a classical key.
An access key with its own gas balance.
A gas key is an ordinary access key with a NEAR balance of its own. Gas for anything the key signs is paid from that balance, not from the account; attached deposits still come from the account. Gas refunds return to the key, deposit refunds to the account.
Two things follow. Any account can top the key up with TransferToGasKey, so an app can
pay a user's gas without running a relayer: fund once, then stay out of the way. And each key carries
1 to 1,024 independent nonce lanes, so one key can send in parallel without nonce
collisions.
NEAR added gas keys in NEP-611 (protocol version 85) so a transaction's fee is guaranteed before it executes; a prepaid, key-scoped balance does that and closes the drain-then-spam gap.
Gas keys ship in @fastnear/api 2.4.0+ — npm i @fastnear/api, or
<script src="https://js.fastnear.com/ for the
near global — with no extra package. The RPC you point at must report
protocol_version 85 or later; check with near.queryProtocolVersion().
Mainnet and testnet both do at the time of writing. Every card on this page signs locally with
sendTx({ signer, signerId }); the wallet path for adding, funding and draining a key is
in the callout below.
-
Fund: anyone.
TransferToGasKey { publicKey, deposit }may be sent by any account. The deposit leaves the sender; the transaction's receiver is the key's owner. The sponsor never needs the key. -
Withdraw: the owner only.
WithdrawFromGasKey { publicKey, amount }must be signed by the owning account and pays out to that account. A sponsor cannot pull a top-up back. -
Delete: the owner, after draining. The ordinary
DeleteKey. It refuses if the key holds more than 1 NEAR and burns whatever is left below that. -
Sign: the key itself, on a lane.
sendTx({ signer, signerId, nonceIndex })detects theGasKey*permission and signs a TransactionV1. Gas from the key, deposits from the account. It cannot sign a NEP-366 delegate.
- Sponsor a user without a relayer. A NEP-366 relayer signs and pays every transaction, so it must be online for each one. A gas-key sponsor funds once and steps away until the balance runs low.
- Many sends from one key. A classical key serializes on its single nonce. Four lanes let four workers send at once.
-
Not an allowance. A function-call key's
allowanceis the account's own NEAR, spent from the account. A gas key is a separate balance any account can refill. -
Not for delegates. A gas key cannot sign a delegate action
(
near.signDelegaterefuses it), andWithdrawFromGasKeyis refused inside a delegate.
Adding, funding and draining a gas key through a wallet works with @fastnear/wallet
2.5.0+ and a near-connect 0.14.1+ wallet whose manifest sets features.gasKeys — Meteor
Wallet, verified on testnet on 2026-09-24. Wallets without the flag are refused rather than risk a
downgraded key. Signing with a gas key (nonceIndex) is always local:
near.signDelegate refuses a gas-key signer and no wallet holds the lane nonce, so every
card on this page signs with sendTx({ signer, signerId }). Also, DeleteKey
burns whatever balance the key still holds (and refuses above 1 NEAR): drain with
WithdrawFromGasKey first. The last card below does both in one transaction.
Add, fund, send, inspect, drain.
Five cards, straight from the gasKeys block of
recipes.json
(ESM), in lifecycle order. Read the first three as one story: the sponsored counter.
A user account adds a gas key scoped to count.mike.testnet / increase — the
contract the home-page demo already calls. A different account,
the sponsor, funds it with TransferToGasKey. The user's client then calls
increase signed by the gas key: the user's account balance is unchanged by the call,
the key's balance drops by the gas burnt, and the gas refund lands back on the key. The sponsor is
not on the path of that call and is only involved again when the balance needs topping up — and any
account, not just the original sponsor, can do that.
Then read the third and fourth cards again as the lanes story. A key added with
numNonces: 4 has four independent nonces. Promise.all over
nonceIndex 0 through 3 sends four transactions at once; queryGasKeyNonces
before and after shows each lane advanced by one. A classical key serializes every send on its
single nonce. A gas key gives one signer many workers — the shape backends and agents want.
Signed by an existing full-access key like any AddKey, after gating on
protocolVersion. The permission reads back as
{ GasKeyFullAccess: { balance: "0", num_nonces: 4 } }. For the sponsored
counter, swap the action for
near.actions.addLimitedAccessGasKey({ publicKey, numNonces, accountId: "count.mike.testnet", methodNames: ["increase"] })
— the balance still starts at "0", and there is no allowance to set.
import { actions, queryAccessKey, queryProtocolVersion, sendTx } from "@fastnear/api";
import { privateKeyFromRandom, signerFromPrivateKey } from "@fastnear/utils";
export async function addGasKey({ accountId, ownerSigner, numNonces = 4 }) {
const protocolVersion = await queryProtocolVersion({ network: "testnet" });
if (protocolVersion < 85) {
throw new Error(`testnet protocol ${protocolVersion} does not support gas keys`);
}
// A gas key is an ordinary ed25519 key pair; only its permission differs.
const gasKeyPrivateKey = privateKeyFromRandom("ed25519");
const gasSigner = signerFromPrivateKey(gasKeyPrivateKey);
await sendTx({
signerId: accountId,
signer: ownerSigner,
receiverId: accountId,
// numNonces = independent transaction lanes (1..1024); the AddKey fee grows with it.
actions: [actions.addFullAccessGasKey({ publicKey: gasSigner.publicKey, numNonces })],
waitUntil: "FINAL",
network: "testnet",
});
const view = await queryAccessKey({
accountId,
publicKey: gasSigner.publicKey,
blockId: "final",
network: "testnet",
});
// view.result.permission -> { GasKeyFullAccess: { balance: "0", num_nonces: 4 } }
return { gasKeyPrivateKey, publicKey: gasSigner.publicKey, permission: view.result.permission };
}
The sponsor's only transaction. signerId is the funder and receiverId
is the key's owner; they may differ, and the deposit leaves the funder. The
read-back goes through gasKeyInfoFromPermission, which returns
null for a classical key; balance is a yoctoNEAR decimal string.
import { actions, gasKeyInfoFromPermission, queryAccessKey, sendTx } from "@fastnear/api";
export async function fundGasKey({ funderId, funderSigner, accountId, publicKey, amount = "0.05 NEAR" }) {
await sendTx({
signerId: funderId,
signer: funderSigner,
receiverId: accountId,
actions: [actions.transferToGasKey({ publicKey, deposit: amount })],
waitUntil: "FINAL",
network: "testnet",
});
const view = await queryAccessKey({ accountId, publicKey, blockId: "final", network: "testnet" });
const info = gasKeyInfoFromPermission(view.result.permission);
if (!info) throw new Error(`${publicKey} is not a gas key on ${accountId}`);
return info.balance; // yoctoNEAR decimal string
}
The user's transaction. signer is the gas key, signerId is the user, and
nonceIndex picks the lane (default 0). sendTx sees the
GasKey* permission and signs a TransactionV1: a 0x01 prefix, a
GasKeyNonce { nonce, nonceIndex }, and a trailing NonceMode
("monotonic" by default, nonce > stored; "strict", exactly
stored + 1). For the counter, receiverId is count.mike.testnet and
methodName is increase. sendOnTwoLanes is the lanes story
in two lines; widen [0, 1] to [0, 1, 2, 3] for a key added with
numNonces: 4.
import { actions, queryGasKeyNonces, sendTx } from "@fastnear/api";
export async function sendWithGasKey({ accountId, gasSigner, receiverId, nonceIndex = 0 }) {
const result = await sendTx({
signerId: accountId,
signer: gasSigner,
receiverId,
actions: [actions.functionCall({ methodName: "ping", args: {}, gas: "30 Tgas", deposit: "0" })],
nonceIndex,
waitUntil: "FINAL",
network: "testnet",
});
const nonces = await queryGasKeyNonces({
accountId,
publicKey: gasSigner.publicKey,
blockId: "final",
network: "testnet",
});
return { txHash: result.transaction?.hash ?? null, nonces: nonces.result.nonces };
}
// Lanes are independent nonce sequences, so these do not serialize behind each other.
export const sendOnTwoLanes = (params) =>
Promise.all([0, 1].map((nonceIndex) => sendWithGasKey({ ...params, nonceIndex })));
Three reads at finality: view_access_key for balance, lane count, and function-call
scope; view_gas_key_nonces for one nonce per lane (the key's own
view_access_key nonce is always 0, and a classical key answers
UNKNOWN_GAS_KEY); the key list to confirm the key is present. Run it before and
after sendOnTwoLanes to watch lanes 0 and 1 each advance by one.
queryAccessKeyList now paginates: pass limit and page with
afterKey; more than 100 keys is refused unpaginated.
import {
gasKeyInfoFromPermission,
queryAccessKey,
queryAccessKeyList,
queryGasKeyNonces,
} from "@fastnear/api";
export async function inspectGasKey({ accountId, publicKey }) {
const [direct, lanes, list] = await Promise.all([
queryAccessKey({ accountId, publicKey, blockId: "final", network: "testnet" }),
queryGasKeyNonces({ accountId, publicKey, blockId: "final", network: "testnet" }),
queryAccessKeyList({ accountId, blockId: "final", network: "testnet" }),
]);
const info = gasKeyInfoFromPermission(direct.result.permission);
if (!info) throw new Error(`${publicKey} is not a gas key on ${accountId}`);
return {
balance: info.balance,
numNonces: info.num_nonces,
nonces: lanes.result.nonces,
// Set only for GasKeyFunctionCall keys.
receiverId: info.functionCall?.receiver_id ?? null,
listed: list.result.keys.some((key) => key.public_key === publicKey),
};
}
One batched transaction signed by the owning account — WithdrawFromGasKey cannot be
signed by anyone else. The withdraw is skipped when the balance is already 0.
DeleteKey refuses above 1 NEAR and burns anything below it, so the order inside
the batch matters.
import { actions, gasKeyInfoFromPermission, queryAccessKey, sendTx } from "@fastnear/api";
export async function withdrawAndDeleteGasKey({ accountId, ownerSigner, publicKey }) {
const view = await queryAccessKey({ accountId, publicKey, blockId: "final", network: "testnet" });
const info = gasKeyInfoFromPermission(view.result.permission);
if (!info) throw new Error(`${publicKey} is not a gas key on ${accountId}`);
// DeleteKey fails above 1 NEAR and burns anything below it, so withdraw first.
// WithdrawFromGasKey must be signed by the owning account itself.
const drain = BigInt(info.balance) > 0n
? [actions.withdrawFromGasKey({ publicKey, amount: info.balance })]
: [];
await sendTx({
signerId: accountId,
signer: ownerSigner,
receiverId: accountId,
actions: [...drain, actions.deleteKey({ publicKey })],
waitUntil: "FINAL",
network: "testnet",
});
const after = await queryAccessKey({ accountId, publicKey, blockId: "final", network: "testnet" });
if (!/UnknownAccessKey|does not exist/i.test(after.result.error ?? "")) {
throw new Error("gas key still present");
}
}
Gate on the protocol, read at finality, drain before you delete.
These rules come from the machine-readable catalog — agents and humans should apply the same ones.
-
Check the selected RPC's active
protocol_versionand require 85 or later before adding or using a gas key; do not use node software versions as the activation signal. - Treat the gas key's private key like any signing secret. Keep recovery records public-only: network, account ID, public key.
-
The balance is the only spend limit on a gas key, and any account can raise it with
TransferToGasKey. If the key must not sign arbitrary actions, scope it withGasKeyFunctionCall'sreceiver_idandmethod_names.
-
Read balances and lane nonces at finality (
blockId: "final") after awaitUntil: "FINAL"send before asserting on them; the default query finality is optimistic. -
Drain with
WithdrawFromGasKeybeforeDeleteKey. A gas refund that lands after your balance read is burnt on deletion; the loss is bounded by 1 NEAR becauseDeleteKeyrefuses above that. -
Gas keys cannot sign NEP-366 delegate actions, and
WithdrawFromGasKeyis refused inside a delegate. Do not route gas-key traffic through a relayer.
AI agents: the gasKeys key in
recipes.json
carries all five quickstarts (gas-key-add, gas-key-fund,
gas-key-send, gas-key-inspect, gas-key-withdraw-delete) plus
the limits, permissionViews, rpc, builders,
rules, and safety on this page. The protocol change is
NEP-611.
For a relayer-paid flow instead, see
meta-transactions; the API lives in
@fastnear/api on npm.
Rate-limited? Free trial credits are at
dashboard.fastnear.com.