Agent ID Gate
A bonding-curve launch can be Agent ID-gated: while the token is on the curve, only wallets that hold an Agent ID can buy or sell it, and each wallet can buy at most 2% of the supply. This page explains how the gate works and how a terminal or wallet should trade a gated token.
The events and reads to index are in Build an Indexer → Agent ID gate and buy cap; how the gate is set at launch is in Extension encoding.
What is an Agent ID
An Agent ID is an ERC-721 collection used as the on-chain identity of an agent. Holding at least one NFT of the collection is the credential to trade that agent's token on the curve.
- The gate is collection-wide: any NFT of the collection counts, not a specific
tokenId. - Only collections whitelisted by Flap can gate a launch (
nftWhitelisted(nft)); a launch with any other collection revertsNFTNotWhitelisted. Example: the Binance Agent ID collection,0x60B9262456dE011F7d15a54CdD8c48bAaa8B022Fon BNB Chain. - The Portal ABI keeps the older
nft…names (nftGate,nftWhitelisted).
How the gate works
Where it applies
| Token state | getTokenV8Safe(token).status | Curve trade | Gate |
|---|---|---|---|
| On the curve | 1 (Tradable) | swapExactInput / swapExactInputV3 | Enforced |
| Migrated to the DEX | 4 (DEX) | Reverts TokenNotTradable | None: the DEX is open to everyone |
The gate applies only to curve buys and sells, never to token transfers or DEX trades. A token is gated when nftGate(token) != address(0). An admin can remove a token's gate after launch (FlapNFTGateUpdated), but never add or change one.
The Agent ID check
Every curve buy and sell of a gated token requires nft.balanceOf(tx.origin) >= 1.
- The check is on
tx.origin, notmsg.sender. - It runs before any payment, so a failed check reverts without moving funds (
NFTGatedOnly(wallet)). - The
balanceOfcall is capped at 100,000 gas. If it reverts, runs out of gas or returns malformed data, the check is skipped and the trade goes through, so a broken collection cannot freeze the curve. Standard ERC-721 collections are not affected.
The buy cap
Gated launches get a cap of 200 bps (2%) of the max supply per tx.origin: 20,000,000 tokens for a 1B-supply token. What counts against it:
held = max(netBought[token][origin], token.balanceOf(origin))
remaining = max(0, cap - held) // buyQuotaOf(token, origin)
netBoughtgrows with every curve buy by the origin (whoever receives the tokens) and shrinks with every curve sell, never below zero.- Transferring tokens out does not lower
netBought, so it does not free the cap. - Receiving tokens raises the balance, so it uses the recipient's cap. A wallet can be pushed to its cap by a gift without buying.
- Sells are never capped, even beyond
netBought(the counter stops at zero). - A buy above the remaining cap is filled up to the cap and the rest of the input is refunded to the caller, in the input currency. At the cap, a buy reverts
BuyCapExceeded(origin, held, cap). When only a few wei remain, a buy can also return0tokens with a full refund: treat that as "cap reached". - The creator's first buy at launch is an ordinary curve buy: it also needs the Agent ID and counts against the cap.
- For a wallet that only buys and sells on the curve itself (no transfers in or out),
netBoughtequals its balance, socap - balanceOf(wallet)is exact. Otherwise readbuyQuotaOf.
Non-gated tokens with a cap use the plain holding rule: held = balanceOf(origin).
Trading a gated token
1. Check whether the user can trade
const [state, nft] = await Promise.all([
portal.getTokenV8Safe(token),
portal.nftGate(token).catch(() => ZeroAddress), // Portals before v5.27.0 do not have it
]);
const gated = state.status === 1n && nft !== ZeroAddress;
let holdsAgentId = true;
let remaining = MaxUint256;
if (gated) {
holdsAgentId = (await erc721(nft).balanceOf(user)) > 0n;
[, remaining] = await portal.buyQuotaOf(token, user);
}
const canBuy = !gated || (holdsAgentId && remaining > 0n);
const canSell = !gated || holdsAgentId;Check the Agent ID yourself: the quote does not.
2. Quote with from set to the user
The cap is read on tx.origin, so simulate as the real trader. Without from, tx.origin is the zero address and the quote assumes the full cap.
const out = await portal.quoteExactInput.staticCall(
{ inputToken: ZeroAddress, outputToken: token, inputAmount: parseEther("1") },
{ from: user },
);- At the cap, the quote reverts
BuyCapExceeded. - Above the remaining cap, the quote returns the capped output (the refund is not part of the result).
3. Swap
await portal.swapExactInput(
{ inputToken: ZeroAddress, outputToken: token, inputAmount: parseEther("1"),
minOutputAmount: (out * 99n) / 100n, permitData: "0x" },
{ value: parseEther("1") },
);Take minOutputAmount from the quote, not from the full input: when the buy is capped the output is smaller, and a minimum computed from the full input reverts SlippageTooHigh. To show the exact refund, simulate the swap and compare the caller's balance; otherwise show "up to X refunded".
Errors
| Error | Cause | Suggested message |
|---|---|---|
NFTGatedOnly(address wallet) | The wallet holds no Agent ID. | "This market requires an Agent ID: hold at least one NFT of {collection} to trade on the curve." |
BuyCapExceeded(address origin, uint256 held, uint256 cap) | The wallet is at its cap. | "Cap reached: {held} of {cap}. Sell or move tokens to buy more." |
SlippageTooHigh(uint256 actual, uint256 minimum) | A capped fill came in below minOutputAmount. | Re-quote with from = user. |
TokenNotTradable(address token) | The token has migrated. | Route the trade to the DEX. |
Limitations
- Relayers and smart accounts. Because the gate reads
tx.origin, trades sent through a relayer, a meta-transaction or an ERC-4337 bundler are checked against that sender, not the user, and normally fail. EIP-7702 delegated wallets keep their owntx.originand work. - Routed buys still count. A buy through a helper contract, or sent to another wallet, uses the origin's cap.
- Agent ID moved away. A holder who transfers their Agent ID can still transfer tokens, but can no longer sell on the curve until they get one back or the token migrates.
Example
A 1B-supply token gated by the Binance Agent ID collection; the cap is 20,000,000 tokens.
- Alice holds an Agent ID and buys with 100 BNB: the fill stops at 20,000,000 tokens and the extra BNB is refunded.
remaining = 0. - She transfers 5,000,000 tokens to another wallet:
netBoughtstays 20,000,000, soremainingstays 0. - She sells 10,000,000 tokens on the curve:
netBoughtdrops to 10,000,000 andremainingis now 10,000,000. - Bob gives Alice 20,000,000 tokens: her balance (25,000,000) is over the cap, so
remaining = 0again. Bob's ownnetBoughtis unchanged: giving tokens away did not free his cap. - Alice transfers her Agent ID away: a curve sell now reverts
NFTGatedOnly. - The token migrates to the DEX: the gate no longer applies, and anyone can trade it there.