Agent ID-Gated Internal Market — Terminal Integration Guide
This guide covers the agent-gated bonding-curve market. An Agent ID is an ERC-721 collection used as an on-chain credential: a wallet holding an NFT from the collection can trade the agent's token on the internal curve. An Agent ID-gated launch also gets a 2% per-origin max-buy cap. It targets Portal v5.27.2; the entry point is the Portal proxy used for swapExactInput.
Contents
- What is an Agent ID
- Overview
- 1. Semantics
- 2. Read API (Portal)
- 3. Events
- 4. Integration recipes (ethers v6)
- 5. Errors
- 6. Launch encoding (for launch tooling)
- 7. Caveats and limitations
- 8. Worked example
- Live examples (BNB Chain)
- 9. ABI fragments
- 10. Integration checklist
- 11. Contract addresses
What is an Agent ID
An Agent ID is an ERC-721 collection used as the on-chain identity of an agent. The NFT is the technical primitive, not the agent itself. Holding at least one NFT from the collection is the credential to trade that agent's token on the internal curve.
- The gate is collection-wide: any NFT from the collection counts; it does not check a specific
tokenId. - Any ERC-721 collection can serve as an Agent ID. Flap only enables whitelisted collections for launches. The ABI keeps the legacy
nftGate/nftWhitelisted/NFT_WHITELIST_ROLEnames. - The gate only applies to the internal curve. After graduation, DEX trading is open to everyone.
Overview
- A launch can opt into an Agent ID gate. While the token is on the curve, every buy and sell
requires
tx.originto hold at least 1 NFT from the configured Agent ID collection. - Agent ID-gated launches are created with a 2%-of-maxSupply max-buy cap per
tx.origin(20,000,000 tokens for a 1B-supply launch). Enabling the gate sets the initial cap to 200 bps; it is not a separate launch flag. - The cap uses
held = max(netBought, balanceOf(tx.origin)): selling reduces the counter (clamped at zero), transferring out does not reducenetBought, and transferring in consumes the recipient's quota via the balance component. - Selling is not capped: selling more than
netBought(for example tokens received by transfer) is allowed, and the counter clamps at zero. - Detect with
nftGate(token): it returns the Agent ID collection address, oraddress(0)for a normal token. - The Agent ID gate is curve-only. After graduation (
status == 4) the Portal swap path revertsTokenNotTradableand the DEX is open to everyone. quoteExactInputenforces the cap but not the Agent ID check. Check the collection'sbalanceOf(user)yourself before enabling the buy/sell button.- Enforcement is keyed on
tx.origin. Direct EOA transactions work; relayers, meta-transactions and ERC-4337 UserOps normally fail because theirtx.originis the relayer or bundler.
1. Semantics
1.1 Where the gate applies
| Token state | getTokenV8Safe(token).status | Portal curve trade | Agent ID gate |
|---|---|---|---|
| On the curve | 1 (Tradable) | swapExactInput / swapExactInputV3 | Enforced |
| Graduated | 4 (DEX) | reverts TokenNotTradable | Not applicable |
| Unknown | 0 (Invalid) | reverts | Not applicable |
The Agent ID gate applies only to internal-curve buys and sells. It does not apply to ERC-20 transfers or DEX trades.
1.2 Agent ID check
Every curve trade on an Agent ID-gated token requires:
nft.balanceOf(tx.origin) >= 1
- The check uses
tx.origin, notmsg.sender. - The
balanceOfcall is aSTATICCALLcapped at 100,000 gas and reads at most 32 bytes, so a large return payload cannot grief the caller. - Fail-open corner case: if
balanceOfreverts, runs out of gas, returns fewer than 32 bytes, or the contract is self-destructed or upgraded to something unreadable, the check is skipped and the trade proceeds. This keeps a broken NFT from causing a DoS on the curve, but it also means the gate is not enforced while the collection is unreadable. - The Agent ID collection address is validated at launch. A zero or codeless address reverts
InvalidNFTGate()(a launch-side error).
1.3 Agent ID buy cap (gated launches only)
An Agent ID-gated launch stores a per-tx.origin buy cap of 200 bps = 2% of maxSupply
(20,000,000 tokens for a 1B launch).
Non-gated capped tokens use the pure holding cap:
held = token.balanceOf(tx.origin)
remaining = max(0, cap - held)
Agent ID-gated tokens use a hybrid:
held = max(netBought[token][tx.origin], token.balanceOf(tx.origin))
remaining = max(0, cap - held) // read from buyQuotaOf(token, origin)
netBoughtis credited on every curve buy totx.origin, regardless of who receives the minted tokens. It is reduced on every curve sell, clamped at zero.- Selling is not capped. Selling more than
netBought(for example tokens received by transfer) is allowed; the counter simply stops at zero. - Transfer out does not reduce
netBought. It can lower the balance component down to thenetBoughtfloor, but it never restores the counter. - Transfer in increases
balanceOf(origin), so it consumes the recipient's quota (the recipient may be unable to buy until it sells or moves the tokens). This is the same holding-cap behavior as non-gated tokens. - A buy counts against
tx.originwhoever receives the tokens. Routing the buy to another wallet or through a helper contract still uses up the origin's 2%. - Shortcut for terminals: if a wallet only ever buys and sells on the curve itself (never
transfers the token out, never receives it by transfer, never routes buys to another address),
netBoughtequals its token balance, soremaining = cap - balanceOf(wallet)is safe to use. Otherwise readbuyQuotaOf. - A buy from an origin with
remaining == 0revertsBuyCapExceeded(origin, held, cap). - A buy from an origin with
remaining > 0fills up to the cap and refunds the excess input in the input currency (native or quote ERC-20). - Edge case: when
remainingis only a few wei of tokens, the buy may returnoutputAmount == 0with a full refund instead of reverting. Treat a zero output as "cap reached". - The creator's
quoteAmtlaunch buy is a normal curve buy: it requires the NFT and is capped. - Non-gated tokens have no automatic cap, but an admin can set one. Always
read
maxBuyPerOrigin; never hardcode 2%. A cap on a non-gated token is a holding cap based on the live balance (transfers do move it), not net buys. - If an admin removes a token's Agent ID gate (
removeTokenNFTGate), the curve opens to everyone and the 2% cap switches to that balance-based holding cap. A gate can only be removed, never added or swapped after launch. WatchFlapNFTGateUpdated. - On tokens gated before v5.27.2 every origin starts with
netBought = 0, so held starts from balance; an origin whose bag was already moved out can buy one full new cap. FlapBuyQuotaUpdatedis emitted only on buys when a cap is active. For Agent ID-gated tokensboughtismax(netBought, balanceOf(origin))after the mint. Sells and transfers change quota without this event; reconstruct withTokenBought/TokenSold/Transferor readbuyQuotaOf.
1.4 Agent ID whitelist
New Agent ID-gated launches only accept collections on the Portal whitelist; otherwise the launch reverts
NFTNotWhitelisted(nft). Launch tooling can call nftWhitelisted(nft) before submitting.
Only collections approved by Flap can be used as an Agent ID, so a launch cannot point at a malicious NFT contract.
2. Read API (Portal)
| Function | Selector |
|---|---|
nftGate(address token) | 0x9b95f8dc |
maxBuyPerOrigin(address token) | 0x4b02c8e9 |
buyQuotaOf(address token, address origin) | 0x929b098c |
getTokenV8Safe(address token) | 0x62fafcca |
nftWhitelisted(address nft) | 0x0bfdb80d |
// address(0) = not gated
function nftGate(address token) external view returns (address nft);
// token-level cap; bps == 0 (and maxBuyAmount == 0) means no cap
function maxBuyPerOrigin(address token) external view returns (uint16 bps, uint256 maxBuyAmount);
// origin-level live allowance; remaining == type(uint256).max when no cap is set
function buyQuotaOf(address token, address origin)
external view returns (uint256 bought, uint256 remaining);
// status: 1 = Tradable (curve), 4 = DEX (graduated); other fields unchanged
function getTokenV8Safe(address token) external view returns (TokenStateV8Safe memory);
// launch whitelist: true = collection may be used as a gate for new launches
function nftWhitelisted(address nft) external view returns (bool allowed);Notes:
- On Agent ID-gated tokens,
buyQuotaOf.boughtismax(netBought, balanceOf(origin)).netBoughtchanges on buys and sells (clamped at zero); the balance component changes on transfers. Transfers in consume quota; transfers out never reducenetBought(they can only lower the balance component). For a wallet that only buys and sells on the curve itself, it equalsbalanceOf(wallet); otherwise readbuyQuotaOf. - On non-gated capped tokens,
buyQuotaOf.boughtisbalanceOf(origin). buyQuotaOf.remaining == type(uint256).maxmeans "no cap configured".- Example Agent ID collection: Binance Agent ID (
0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F). nftGatereverts on pre-v5.27.0implementations because the selector does not exist. Feature-detect it:
async function readNftGate(portal: Contract, token: string): Promise<string | null> {
try {
return await portal.nftGate(token);
} catch {
return null; // feature not live on this Portal implementation
}
}3. Events
| Event | topic0 |
|---|---|
FlapNFTGated(address token, address nft) | 0x2179156b3d2ade0f0de96eff28a7c29d924002784e113e69108d91a02cbf9098 |
FlapTokenMaxBuyPerOriginSet(address indexed token, uint16 bps, uint256 maxBuyAmount) | 0xafcebb99a3d0047f96f0d1cc8872781aaeef86f4f5e7e1c6a33e5907d6a3ed82 |
LaunchedToDEX(address token, address pool, uint256 amount, uint256 eth) | 0x6e4f47630b8745b8cacbd44f42a8a33e7eea7cc08ef22fc7630f4f385784ff7d |
FlapNFTWhitelistUpdated(address nft, bool allowed) | 0x9b31e0304b48df87633ad2120618592c707a1830e269c0bc8b435c5ee2f3a47d |
FlapBuyQuotaUpdated(address indexed token, address indexed origin, uint256 bought, uint256 remaining) | 0x6d7a33547efbe801870d2426853a4aa2ae198acb815783fb3fc3d853a944202b |
FlapNFTGateUpdated(address token, address oldNft, address newNft) | 0xef4dac2f9b1190c744c50eaead877aa2f222bafb1ef8bb2c60baed13f4b63cd8 |
FlapNFTGatedis emitted once at launch when the gate is configured. It has no indexed fields; decode(token, nft)fromdata.FlapTokenMaxBuyPerOriginSetis emitted at launch only whenbps > 0; no event whenbps == 0(no cap).maxBuyPerOriginis the authoritative value.LaunchedToDEXmarks graduation. After it, stop applying the gate and route to the DEX.FlapNFTWhitelistUpdatedfires when the admin whitelist adds or removes a collection.FlapBuyQuotaUpdatedfires after every curve buy on a capped token. On Agent ID-gated tokensboughtismax(netBought, balanceOf(origin))after this buy. It is not emitted on sells or transfers; those can change the quota, so callbuyQuotaOf(or trackTokenBought/TokenSold/Transfer) to refreshremaining.FlapNFTGateUpdatedfires when an admin removes a token's Agent ID gate.newNftis alwaysaddress(0). Treat the token as non-gated from then on.TokenBought/TokenSoldare unchanged.
4. Integration recipes (ethers v6)
4.1 Detect the Agent ID gate and the user's eligibility
const portal = new ethers.Contract(PORTAL_ADDRESS, PORTAL_ABI, provider);
const [state, nft, cap] = await Promise.all([
portal.getTokenV8Safe(token),
portal.nftGate(token).catch(() => ethers.ZeroAddress), // pre-v5.27.0 Portal
portal.maxBuyPerOrigin(token),
]);
const onCurve = state.status === 1n; // Tradable
const isGated = onCurve && nft !== ethers.ZeroAddress;
let userHoldsNft = true;
let remaining = ethers.MaxUint256;
if (isGated) {
const erc721 = new ethers.Contract(nft, ["function balanceOf(address) view returns (uint256)"], provider);
userHoldsNft = (await erc721.balanceOf(user)) > 0n;
[, remaining] = await portal.buyQuotaOf(token, user);
}
const canBuy = !isGated || (userHoldsNft && remaining > 0n);
// sell: only the Agent ID holder check matters4.2 Quote (always pass from)
The cap is evaluated on tx.origin, so the eth_call must set from to the real trader. Without
from, tx.origin is address(0), whose balance and netBought are 0, so the quote assumes
the full cap.
const params = {
inputToken: ethers.ZeroAddress, // native BNB/ETH
outputToken: token,
inputAmount: parseEther("1"),
};
const out = await portal.quoteExactInput.staticCall(params, { from: user });quoteExactInputdoes not check the Agent ID. A non-holder still gets a quote; gate the UI withbalanceOf(section 4.1).- If the user is at or over the cap, the quote reverts
BuyCapExceeded. Treat it as "cap reached". - If the input exceeds the remaining allowance, the quote reflects the capped fill; the excess is refunded on execution. The quote returns only the token output, not the refund.
4.3 Swap
The trade ABI is unchanged:
struct ExactInputParams {
address inputToken; // address(0) = native
address outputToken; // address(0) = native
uint256 inputAmount;
uint256 minOutputAmount;
bytes permitData;
}
function swapExactInput(ExactInputParams calldata params)
external payable returns (uint256 outputAmount);Buy with native quote: inputToken = address(0), outputToken = token, msg.value = inputAmount.
Sell: inputToken = token, outputToken = address(0); approve or permit the Portal first.
const tx = await portal.swapExactInput(
{
inputToken: ethers.ZeroAddress,
outputToken: token,
inputAmount: parseEther("1"),
minOutputAmount: (out * 99n) / 100n,
permitData: "0x",
},
{ value: parseEther("1") }
);The Agent ID check runs before payment and before any native↔quote swap, so a failed gate reverts without moving funds. The cap check runs inside the curve math; its revert also unwinds the whole transaction.
4.4 Refund on a capped buy
A below-cap buy is filled up to the cap and the excess input is refunded to msg.sender in the
input currency:
- Native input → native refund.
- ERC-20 quote input → quote-token refund.
Do not derive minOutputAmount from the full input when the input exceeds the remaining allowance:
the actual fill is smaller and a too-high minimum reverts SlippageTooHigh. Quote with from = user
and set the minimum from that result. If the UI needs an exact refund preview, simulate the swap and
diff the caller's native or quote balance (eth_simulateV1, a trace, or a local fork); otherwise show
an approximate "up to X refunded" note.
5. Errors
| Selector | Error | Cause | Suggested UI |
|---|---|---|---|
0xf38fb4a7 | NFTGatedOnly(address wallet) | wallet holds no NFT | "This market requires an Agent ID. Hold at least 1 NFT from {collection} to trade on the curve." |
0xc866db66 | BuyCapExceeded(address origin, uint256 held, uint256 cap) | origin's held (balance or netBought) is >= cap | "Cap reached: {held} of {cap}. Sell or move tokens to free quota." |
0x76baadda | SlippageTooHigh(uint256 actual, uint256 minimum) | capped fill below minOutputAmount | Re-quote with from = user and lower the minimum. |
0x6e8698f2 | TokenNotTradable(address token) | token graduated or not on the curve | Route the trade to the DEX pool. |
0x9843f01f | InvalidNFTGate() | launch config had a zero or codeless NFT | Launch-side error; not expected during trading. |
0x5166d46c | NFTNotWhitelisted(address nft) | launch used a collection not on the whitelist | Launch-side error; pick a whitelisted collection. |
ethers v6 decode:
try {
await portal.swapExactInput(params, { value });
} catch (e: any) {
const data = e?.data ?? e?.info?.error?.data;
const selector = data?.slice(0, 10);
if (selector === "0xf38fb4a7") showNftGateModal();
if (selector === "0xc866db66") showCapModal();
}6. Launch encoding (for launch tooling)
The Agent ID gate travels in the existing extensionID / extensionData launch fields. Two encodings are
accepted.
EXTENSION_ARRAY_ID = keccak256("flap.extensions.v1")
= 0xc87a159bf445e83477134d6374216c321695c2f3e23ee3351c8d3df6ec740253
NFT_GATED_EXTENSION_ID = keccak256("flap.extension.nft_gated.v1")
= 0x9c312f81d9a1b68ba5472ca93c5447d57f897fbf4a8c468b315a67fe8016d1ab
CURVE_AUTH_EXTENSION_ID_V2 = keccak256("flap.curve.auth.v2")
= 0x0f1f338177d85790dd81a6e795b59ae3a205cb498e034240ce2261bf753f13a4
6.1 Array form (recommended; coexists with signed-price)
struct Extension {
bytes32 extensionId;
bytes extensionData;
}
Extension[] memory exts = new Extension[](1);
exts[0] = Extension({
extensionId: NFT_GATED_EXTENSION_ID,
extensionData: abi.encode(nftAddress)
});
params.extensionID = EXTENSION_ARRAY_ID;
params.extensionData = abi.encode(exts);TypeScript (viem):
import { encodeAbiParameters, keccak256, toHex } from "viem";
const NFT_GATED_ID = keccak256(toHex("flap.extension.nft_gated.v1"));
const ARRAY_ID = keccak256(toHex("flap.extensions.v1"));
const extensionData = encodeAbiParameters(
[{ type: "tuple[]", components: [
{ name: "extensionId", type: "bytes32" },
{ name: "extensionData", type: "bytes" },
]}],
[[{
extensionId: NFT_GATED_ID,
extensionData: encodeAbiParameters([{ type: "address" }], [nft]),
}]]
);
// params.extensionID = ARRAY_ID; params.extensionData = extensionData;6.2 Legacy single-extension form
params.extensionID = NFT_GATED_EXTENSION_ID;
params.extensionData = abi.encode(nftAddress);Rules:
nftAddressmust have code at launch; zero or codeless revertsInvalidNFTGate().nftAddressmust be on the launch whitelist for new launches; otherwise the launch revertsNFTNotWhitelisted(nft).- Only the first entry matching
NFT_GATED_EXTENSION_IDis used. - A launch without either encoding has
nftGate == address(0)and no automatic cap. - Agent ID-gated launches set
maxBuyBpsPerOrigin = 200(2%). The creator'squoteAmtbuy also requires the NFT and is capped bymax(netBought, balanceOf(origin)).
7. Caveats and limitations
tx.originidentity. The Agent ID gate assumes an EOA calls the Portal directly.- Backend relayers / meta-transactions:
tx.originis the relayer. - ERC-4337 UserOps:
tx.originis the bundler EOA. - Safe / smart-account execution through a relayer has the same problem.
- In all these cases a real holder's trade reverts
NFTGatedOnly. - EIP-7702 delegated EOAs keep the EOA as
tx.origin; the check still works.
- Backend relayers / meta-transactions:
- Buys always count against
tx.origin. A buy routed through a helper contract or sent to another wallet still uses up the origin's 2%, even though the origin never holds the tokens. - Hybrid cap for gated tokens.
held = max(netBought, balanceOf(origin)). Only curve buys and sells changenetBought; transferring out does not free the counter, while transferring in consumes quota via the balance component. A user can be pushed over the cap by an airdrop/gift without buying. A legacy bag moved out before the upgrade is not counted bynetBought; that origin can buy one full new cap. - Sells are not capped. Selling more than
netBought(for example tokens received by transfer) is allowed and clamps the counter at zero. A recipient's later sale does not reduce the sender'snetBought. - Agent ID transferred away = cannot sell on the curve. Token transfers remain allowed. The holder must re-acquire the NFT or wait for graduation.
- Gas-capped
balanceOf. The call is capped at 100,000 gas. If a collection'sbalanceOfreverts, runs out of gas, returns a short value, or the contract is self-destructed, the gate fails open and the trade proceeds. Standard ERC-721 implementations are fine. - Quote is not the gate.
quoteExactInputnever checks the Agent ID; it does check the cap. Gate the UI yourself and simulate withfrom = user.
8. Worked example
- Example Agent ID collection: Binance Agent ID (
0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F). maxSupply = 1e27(1B tokens, 18 decimals); gated cap =1e27 * 200 / 10000 = 2e25(20,000,000 tokens).- Alice holds the Agent ID, balance 0, buys with
100 BNB:- The fill stops at 20,000,000 tokens; the excess BNB is refunded.
buyQuotaOf(token, alice)→bought = 2e25(netBought = 2e25),remaining = 0.
- Alice transfers 5,000,000 tokens to another wallet:
- Her
netBoughtstays 20,000,000;held = max(2e25, 1.5e25) = 2e25;remainingstays 0. - The recipient's balance 5,000,000 counts against the recipient's quota.
- Her
- Alice sells 10,000,000 tokens back to the curve:
netBought = 1e25; balance = 5,000,000;held = max(1e25, 5e24) = 1e25;remaining = 1e25(10,000,000 more). She holds 5,000,000.
- Bob buys 20,000,000 tokens and transfers all of them to Alice:
- Alice's balance is 25,000,000;
netBoughtis 10,000,000;held = 2.5e25 > cap;remaining = 0. - Alice can sell the 25,000,000 tokens:
netBoughtclamps to 0 and her quota returns to 2e25. - Bob's
netBoughtstays2e25: giving his tokens away did not free his allowance.
- Alice's balance is 25,000,000;
- Alice transfers the Agent ID away:
- She can still transfer tokens, but a curve sell reverts
NFTGatedOnly(alice).
- She can still transfer tokens, but a curve sell reverts
- The token graduates (
LaunchedToDEX,status == 4):- The Portal trade path closes; trade on the DEX with no NFT requirement from Flap.
Live examples (BNB Chain)
| Token | NFT |
|---|---|
0xe0ec675392020EF6e8C871A9822ec50b90B37777 | Test Agent ID NFT: 0xfDE93258a9E3896CAc41B5cf1f7530aD806961e6. The contract is open source; call mint(address to) to claim one for free. |
0x4131Cae06908e7D874cBA44004d8803045367777 | Binance Agent ID NFT: 0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F. |
9. ABI fragments
9.1 Human-readable
function nftGate(address token) view returns (address nft)
function maxBuyPerOrigin(address token) view returns (uint16 bps, uint256 maxBuyAmount)
function buyQuotaOf(address token, address origin) view returns (uint256 bought, uint256 remaining)
function nftWhitelisted(address nft) view returns (bool allowed)
event FlapNFTGated(address token, address nft)
event FlapNFTWhitelistUpdated(address nft, bool allowed)
event FlapTokenMaxBuyPerOriginSet(address indexed token, uint16 bps, uint256 maxBuyAmount)
event LaunchedToDEX(address token, address pool, uint256 amount, uint256 eth)
event FlapBuyQuotaUpdated(address indexed token, address indexed origin, uint256 bought, uint256 remaining)
event FlapNFTGateUpdated(address token, address oldNft, address newNft)
error NFTGatedOnly(address wallet)
error NFTNotWhitelisted(address nft)
error BuyCapExceeded(address origin, uint256 held, uint256 cap)
error InvalidNFTGate()
9.2 JSON ABI (new surface only)
[
{
"type": "function",
"name": "nftGate",
"stateMutability": "view",
"inputs": [{ "name": "token", "type": "address" }],
"outputs": [{ "name": "nft", "type": "address" }]
},
{
"type": "function",
"name": "maxBuyPerOrigin",
"stateMutability": "view",
"inputs": [{ "name": "token", "type": "address" }],
"outputs": [
{ "name": "bps", "type": "uint16" },
{ "name": "maxBuyAmount", "type": "uint256" }
]
},
{
"type": "function",
"name": "buyQuotaOf",
"stateMutability": "view",
"inputs": [
{ "name": "token", "type": "address" },
{ "name": "origin", "type": "address" }
],
"outputs": [
{ "name": "bought", "type": "uint256" },
{ "name": "remaining", "type": "uint256" }
]
},
{
"type": "function",
"name": "nftWhitelisted",
"stateMutability": "view",
"inputs": [{ "name": "nft", "type": "address" }],
"outputs": [{ "name": "allowed", "type": "bool" }]
},
{
"type": "event",
"name": "FlapNFTGated",
"inputs": [
{ "name": "token", "type": "address", "indexed": false },
{ "name": "nft", "type": "address", "indexed": false }
]
},
{
"type": "event",
"name": "FlapNFTWhitelistUpdated",
"inputs": [
{ "name": "nft", "type": "address", "indexed": false },
{ "name": "allowed", "type": "bool", "indexed": false }
]
},
{
"type": "event",
"name": "FlapTokenMaxBuyPerOriginSet",
"inputs": [
{ "name": "token", "type": "address", "indexed": true },
{ "name": "bps", "type": "uint16", "indexed": false },
{ "name": "maxBuyAmount", "type": "uint256", "indexed": false }
]
},
{
"type": "error",
"name": "NFTGatedOnly",
"inputs": [{ "name": "wallet", "type": "address" }]
},
{
"type": "error",
"name": "NFTNotWhitelisted",
"inputs": [{ "name": "nft", "type": "address" }]
},
{
"type": "error",
"name": "BuyCapExceeded",
"inputs": [
{ "name": "origin", "type": "address" },
{ "name": "held", "type": "uint256" },
{ "name": "cap", "type": "uint256" }
]
},
{
"type": "event",
"name": "FlapBuyQuotaUpdated",
"inputs": [
{ "name": "token", "type": "address", "indexed": true },
{ "name": "origin", "type": "address", "indexed": true },
{ "name": "bought", "type": "uint256", "indexed": false },
{ "name": "remaining", "type": "uint256", "indexed": false }
]
},
{
"type": "event",
"name": "FlapNFTGateUpdated",
"inputs": [
{ "name": "token", "type": "address", "indexed": false },
{ "name": "oldNft", "type": "address", "indexed": false },
{ "name": "newNft", "type": "address", "indexed": false }
]
}
]getTokenV8Safe, quoteExactInput, swapExactInput, TokenBought, and TokenSold are unchanged;
reuse the ABI you already integrate.
10. Integration checklist
- Feature-detect
nftGatewith try/catch; handle pre-v5.27.0Portals. - On token load, read
getTokenV8Safe.status,nftGate,maxBuyPerOrigin, andbuyQuotaOf(token, user). - If
status == 1andnftGate != 0, show the Agent ID banner, the collection, and the user's holder status. - Disable buy when
nft.balanceOf(user) == 0orremaining == 0. - Disable sell when
nft.balanceOf(user) == 0; sells are not capped bynetBought. - Quote with
from = userfor a cap-accurate result; remember the quote does not check the NFT. - Submit gated trades as direct EOA transactions; avoid relayers and 4337 bundlers.
- Handle
NFTGatedOnly,BuyCapExceeded,SlippageTooHigh, andTokenNotTradable. The ABI keeps theNFTnames; Agent ID is the product term. - Show a live cap meter (
bought / cap,remaining) and refresh it after curve buys, sells, and transfers. - For launch tooling, check
nftWhitelisted(nft)for the Agent ID collection before submitting; handleNFTNotWhitelisted. - Listen for
FlapNFTWhitelistUpdatedwhen showing launch eligibility. - On
LaunchedToDEXorstatus == 4, switch to DEX routing and remove the gate UI. - Index
FlapNFTGatedso Agent ID-gated tokens are known before the user opens them, andFlapNFTGateUpdatedto drop the Agent ID UI when an admin removes it.
11. Contract addresses
| Chain | Chain ID | Portal proxy |
|---|---|---|
| BNB Smart Chain mainnet | 56 | 0xe2cE6ab80874Fa9Fa2aAE65D277Dd6B8e65C9De0 |
| BNB Smart Chain testnet | 97 | 0x5bEacaF7ABCbB3aB280e80D007FD31fcE26510e9 |
Other chains use different Portal proxies; take the address from Flap's deployed-contract addresses page. The ABI and semantics above are identical on every chain.