Agent ID 门控内盘 — Terminal 集成指南
本文介绍 Agent ID 门控的 bonding-curve 内盘。Agent ID 是一个 ERC-721 合集,作为 agent 的链上身份凭证:持有该合集中任意 NFT 的钱包,可以在内盘交易对应 agent 的代币。门控发射还会带上按 tx.origin 计算的 2% 买入上限。适用 Portal v5.27.2,入口为 swapExactInput 使用的 Portal 代理合约。
目录
- 什么是 Agent ID
- 概览
- 1. 规则
- 2. 读取接口(Portal)
- 3. 事件
- 4. 集成示例(ethers v6)
- 5. 错误
- 6. 发射编码(面向发射工具)
- 7. 限制与边界条件
- 8. 示例
- 链上案例(BNB Chain)
- 9. ABI 片段
- 10. 集成清单
- 11. 合约地址
什么是 Agent ID
Agent ID 是一个 ERC-721 合集,用来表示 agent 的链上身份。NFT 是技术载体,agent 本身不是这个 NFT。持有该合集中至少一个 NFT,就拥有在内盘交易该 agent 代币的凭证。
- 门控是 collection-wide 的:持有该合集任意一个 NFT 即可,不校验具体
tokenId。 - 任何 ERC-721 合集都可以作为 Agent ID;Flap 只会在白名单内开启发射。ABI 继续使用
nftGate/nftWhitelisted/NFT_WHITELIST_ROLE这些旧名字。 - 门控只作用于内盘曲线;毕业后 DEX 对所有人开放。
概览
- 发射方可为代币启用 Agent ID 门控。代币在曲线上期间,每笔买入和卖出都要求
tx.origin持有指定 Agent ID 合集中至少 1 个 NFT。 - Agent ID 门控代币在创建时会带上 按
tx.origin计算的 maxSupply 2% 买入上限(1B 供应量对应 20,000,000 代币)。启用门控即把初始上限设为 200 bps,不是两个独立开关。 - 额度使用
held = max(netBought, balanceOf(tx.origin)):卖出会减少计数(最低到 0),转出 不会减少netBought,转入会通过余额占用接收方的额度。 - 卖出不受限制:卖出超过
netBought的代币(例如别人转给你的币)是允许的,计数最低只会到 0。 - 用
nftGate(token)检测:返回 Agent ID 合集地址;普通代币返回address(0)。 - Agent ID 门控仅作用于曲线阶段。毕业后(
status == 4)Portal 交易路径返回TokenNotTradable, DEX 交易对所有人开放。 quoteExactInput会检查买入上限,但不检查 Agent ID。启用买卖按钮前请自行校验nft.balanceOf(user)。- 校验基于
tx.origin。EOA 直接发交易可以通过;中继器、meta-transaction 和 ERC-4337 UserOp 通常会失败,因为它们的tx.origin是中继器或 bundler。
1. 规则
1.1 生效范围
| 代币状态 | getTokenV8Safe(token).status | Portal 曲线交易 | Agent ID 门控 |
|---|---|---|---|
| 在曲线上 | 1(Tradable) | swapExactInput / swapExactInputV3 | 生效 |
| 已毕业 | 4(DEX) | 返回 TokenNotTradable | 不适用 |
| 未知 | 0(Invalid) | revert | 不适用 |
Agent ID 门控只作用于内盘曲线的买入和卖出,不影响 ERC-20 转账和 DEX 交易。
1.2 Agent ID 校验
Agent ID 门控代币的每笔曲线交易都要求:
nft.balanceOf(tx.origin) >= 1
- 校验对象是
tx.origin,不是msg.sender。 balanceOf通过STATICCALL调用,gas 上限 100,000,最多读取 32 字节,避免恶意合集用超大返回值 消耗调用方 gas。- Fail-open 边界情况: 如果
balanceOfrevert、gas 不足、返回不足 32 字节,或合约自毁 / 升级后无法读取,校验会被跳过,交易继续执行。这样坏掉的 NFT 不会对曲线造成 DoS,但也意味着合集不可读时 门控不生效。 - Agent ID 合集地址在发射时校验。零地址或无代码地址返回
InvalidNFTGate()(发射侧错误)。
1.3 Agent ID 买入上限(仅门控发射)
Agent ID 门控发射会写入按 tx.origin 计算的买入上限:200 bps = maxSupply 的 2%(1B 供应量对应
20,000,000 代币)。
非门控的可设上限代币使用纯持仓上限:
held = token.balanceOf(tx.origin)
remaining = max(0, cap - held)
Agent ID 门控代币使用混合公式:
held = max(netBought[token][tx.origin], token.balanceOf(tx.origin))
remaining = max(0, cap - held) // 从 buyQuotaOf(token, origin) 读取
netBought在每笔曲线买入时记到tx.origin,无论代币最后发给谁;每笔曲线卖出会减少它,最低到 0。- 卖出不受 netBought 限制。 卖出超过 netBought 的代币(例如别人转给你的币)是允许的,计数最低只会到 0。
- 转出不会减少
netBought;它最多降低余额部分到 netBought 的底,不会恢复计数。 - 转入会增加
balanceOf(origin),因此会占用接收方的额度(接收方在卖出或转走之前可能无法买入)。 这和非门控代币的持仓上限行为一致。 - 买入始终记在
tx.origin名下,与代币最终到达哪个地址无关。把买入路由到另一个钱包或经辅助合约 买入,仍会消耗 origin 的 2% 额度。 - Terminal 简便算法: 如果钱包只在曲线上自行买卖(从未转出该代币、从未通过转账收到、也从未把
买入路由到其他地址),
netBought就等于其代币余额,可以放心使用remaining = cap - balanceOf(wallet)。否则请读取buyQuotaOf。 remaining == 0的 origin 再买入,返回BuyCapExceeded(origin, held, cap)。remaining > 0的 origin 买入会成交到上限为止,多余输入按原币种退回(原生币或 quote ERC-20)。- 边界情况:
remaining只剩几 wei 代币时,买入可能不 revert 而是返回outputAmount == 0并全额退款。请把输出为 0 也视为"已达上限"。 - 创建者的
quoteAmt首发买入按普通曲线买入处理:同样需要 NFT,同样受上限约束。 - 非门控代币没有自动上限,但管理员可手动设置。请始终读取
maxBuyPerOrigin,不要硬编码 2%。非门控代币的上限是基于实时余额的持仓上限(转账会影响 额度),不是净买入量。 - 如果 admin 移除了代币的 Agent ID 门控(
removeTokenNFTGate),曲线对所有人开放,2% 上限改按上述基于 余额的持仓上限执行。门控只能移除,发射后不能新增或替换。请监听FlapNFTGateUpdated。 - v5.27.2 之前发射的 legacy 代币
netBought = 0,现有余额通过balanceOf计入。若整个老 bag 被转出,该 origin 可以再买一次满额,之后 netBought 封顶。 FlapBuyQuotaUpdated只在有 cap 的买入时 emit。Agent ID 门控代币的bought是 mint 后的max(netBought, balanceOf(origin))。卖出和转账会改变额度但不发这个事件;请用TokenBought/TokenSold/Transfer或buyQuotaOf重建。
1.4 Agent ID 白名单
新的 Agent ID 门控发射只接受 Portal 白名单中的合集;否则发射会返回 NFTNotWhitelisted(nft)。
发射工具可以在提交前调用 nftWhitelisted(nft) 检查。
只有经 Flap 批准的合集才能作为 Agent ID,避免发射时指向恶意的 NFT 合约。
2. 读取接口(Portal)
| 函数 | 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) = 未门控
function nftGate(address token) external view returns (address nft);
// 代币级上限;bps == 0(且 maxBuyAmount == 0)表示无上限
function maxBuyPerOrigin(address token) external view returns (uint16 bps, uint256 maxBuyAmount);
// origin 级实时额度;无上限时 remaining == type(uint256).max
function buyQuotaOf(address token, address origin)
external view returns (uint256 bought, uint256 remaining);
// status:1 = Tradable(曲线),4 = DEX(已毕业);其余字段不变
function getTokenV8Safe(address token) external view returns (TokenStateV8Safe memory);
// 发射白名单:true = 该合集可用于新的门控发射
function nftWhitelisted(address nft) external view returns (bool allowed);说明:
- Agent ID 门控代币上,
buyQuotaOf.bought是max(netBought, balanceOf(origin))。netBought在买入和 卖出时变化(最低到 0);余额部分会随转账变化。转入会占用额度;转出不会减少netBought(最多 降低余额部分)。对只在曲线上自行买卖的钱包,它等于balanceOf(wallet);否则请读取buyQuotaOf。 - 非门控的有上限代币上,
buyQuotaOf.bought就是balanceOf(origin)。 buyQuotaOf.remaining == type(uint256).max表示未配置上限。- 示例 Agent ID 合集:Binance Agent ID(
0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F)。 v5.27.0之前的实现没有nftGate这个 selector,调用会 revert。请做特性探测:
async function readNftGate(portal: Contract, token: string): Promise<string | null> {
try {
return await portal.nftGate(token);
} catch {
return null; // 当前 Portal 实现不支持该接口
}
}3. 事件
| 事件 | 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 |
FlapNFTGated:发射时配置门控则触发一次。没有 indexed 字段,从data中解码(token, nft)。FlapTokenMaxBuyPerOriginSet:发射时仅在bps > 0时触发;bps == 0(无上限)不触发。 以maxBuyPerOrigin的返回值为准。LaunchedToDEX:毕业事件。此后停止应用门控,切换到 DEX 路由。FlapNFTWhitelistUpdated:admin 增加或移除白名单合集时发出。FlapBuyQuotaUpdated:有上限的代币每次曲线买入后发出。Agent ID 门控代币上bought是本次买入后max(netBought, balanceOf(origin))。卖出和转账时不会发出,之后请调用buyQuotaOf(或自行 跟踪TokenBought/TokenSold/Transfer)刷新remaining。FlapNFTGateUpdated:admin 移除代币的 Agent ID 门控时发出,newNft始终为address(0)。此后按非门控 代币处理。TokenBought/TokenSold不变。
4. 集成示例(ethers v6)
4.1 检测 Agent ID 门控与用户资格
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), // 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);
// 卖出仅需 Agent ID holder 校验4.2 报价(必须传 from)
买入上限按 tx.origin 计算,eth_call 必须把 from 设为真实交易者。不传 from 时
tx.origin 是 address(0),余额和 netBought 都是 0,报价会按完整上限估算。
const params = {
inputToken: ethers.ZeroAddress, // 原生 BNB/ETH
outputToken: token,
inputAmount: parseEther("1"),
};
const out = await portal.quoteExactInput.staticCall(params, { from: user });quoteExactInput不检查 Agent ID。非持有者也能拿到报价,UI 需用 4.1 的balanceOf控制。- 用户已达或超过上限时,报价返回
BuyCapExceeded,按"已达买入上限"处理。 - 输入超过剩余额度时,报价对应上限内的实际成交量,多余输入在执行时退回。报价只返回代币输出量, 不返回退款金额。
4.3 交易
交易 ABI 不变:
struct ExactInputParams {
address inputToken; // address(0) = 原生币
address outputToken; // address(0) = 原生币
uint256 inputAmount;
uint256 minOutputAmount;
bytes permitData;
}
function swapExactInput(ExactInputParams calldata params)
external payable returns (uint256 outputAmount);原生币买入:inputToken = address(0),outputToken = token,msg.value = inputAmount。
卖出:inputToken = token,outputToken = address(0),先对 Portal 做 approve 或 permit。
const tx = await portal.swapExactInput(
{
inputToken: ethers.ZeroAddress,
outputToken: token,
inputAmount: parseEther("1"),
minOutputAmount: (out * 99n) / 100n,
permitData: "0x",
},
{ value: parseEther("1") }
);Agent ID 校验在付款和原生币↔quote 兑换之前执行,门控失败时 revert 且不动用资金。买入上限校验在 曲线计算内部执行,其 revert 同样会回滚整笔交易。
4.4 触顶退款
未达上限的买入会成交到上限为止,多余输入按原币种退给 msg.sender:
- 原生币输入 → 原生币退款。
- ERC-20 quote 输入 → quote 代币退款。
输入超过剩余额度时,不要按完整输入推算 minOutputAmount:实际成交量更小,最小值设得过高会返回
SlippageTooHigh。请用 from = user 报价,并以该结果设置最小值。如需精确的退款预估,可模拟交易并
对比调用者的原生币或 quote 余额(eth_simulateV1、trace 或本地 fork);否则显示"最多退回 X"的近似值。
5. 错误
| Selector | 错误 | 原因 | 建议 UI |
|---|---|---|---|
0xf38fb4a7 | NFTGatedOnly(address wallet) | wallet 未持有 NFT | "该市场需要 Agent ID。请持有至少 1 个 {collection} NFT 后再在曲线上交易。" |
0xc866db66 | BuyCapExceeded(address origin, uint256 held, uint256 cap) | origin 的 held(余额或 netBought)>= cap | "已达上限:{held} / {cap}。卖出/转走部分代币可释放额度。" |
0x76baadda | SlippageTooHigh(uint256 actual, uint256 minimum) | 触顶后的成交量低于 minOutputAmount | 用 from = user 重新报价并调低最小值。 |
0x6e8698f2 | TokenNotTradable(address token) | 代币已毕业或不在曲线上 | 切换到 DEX 池路由。 |
0x9843f01f | InvalidNFTGate() | 发射配置的 NFT 为零地址或无代码 | 发射侧错误,交易中不应出现。 |
0x5166d46c | NFTNotWhitelisted(address nft) | 发射使用了不在白名单中的合集 | 发射侧错误;改用白名单中的 Agent ID 合集。 |
ethers v6 解码:
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. 发射编码(面向发射工具)
Agent ID 门控通过现有的 extensionID / extensionData 发射字段传递,支持两种编码。
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 数组形式(推荐,可与 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 旧版单 extension 形式
params.extensionID = NFT_GATED_EXTENSION_ID;
params.extensionData = abi.encode(nftAddress);规则:
nftAddress在发射时必须有代码;零地址或无代码返回InvalidNFTGate()。- 新发射的
nftAddress必须在发射白名单中;否则返回NFTNotWhitelisted(nft)。 - 只使用第一个匹配
NFT_GATED_EXTENSION_ID的条目。 - 两种编码都不带的发射:
nftGate == address(0),无自动上限。 - Agent ID 门控发射写入
maxBuyBpsPerOrigin = 200(2%)。创建者的quoteAmt首发买入同样需要 NFT, 同样受max(netBought, balanceOf(origin))上限约束。
7. 限制与边界条件
tx.origin身份。 Agent ID 门控假设由 EOA 直接调用 Portal。- 后端中继器 / meta-transaction:
tx.origin是中继器。 - ERC-4337 UserOp:
tx.origin是 bundler EOA。 - Safe / 智能账户经中继器执行:同样受限。
- 上述场景下,真实持有者的交易会返回
NFTGatedOnly。 - EIP-7702 委托后的 EOA:
tx.origin仍是该 EOA,校验正常。
- 后端中继器 / meta-transaction:
- 买入始终计入
tx.origin。 经辅助合约买入或把代币发到另一个钱包,即使 origin 从未持有这些 代币,也会消耗 origin 的 2% 额度。 - 门控代币是混合额度。
held = max(netBought, balanceOf(origin))。只有曲线买入和卖出会改变netBought;转出不会释放计数,转入会通过余额占用额度。用户可能没有买入也被空投/转账推到上限 之上。升级前已转走的老 bag 不计入 netBought,该 origin 可以再买一次满额。 - 卖出不受 netBought 限制。 卖出超过
netBought的代币(例如别人转入的代币)是允许的,计数最低 到 0。接收方之后卖出也不会减少 sender 的netBought。 - Agent ID 转出后无法在曲线上卖出。 代币转账仍然可用;持有者需重新取得 Agent ID 或等待毕业。
balanceOf有 gas 上限。 调用上限为 100,000 gas。若合集的balanceOfrevert、gas 不足、 返回值不足 32 字节,或合约自毁,门控会 fail open,交易继续执行。标准 ERC-721 实现不受影响。- 报价不等于门控。
quoteExactInput从不检查 Agent ID,只检查买入上限。UI 需自行校验,并用from = user模拟。
8. 示例
- 示例 Agent ID 合集:Binance Agent ID(
0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F)。 maxSupply = 1e27(1B 代币,18 位小数);门控上限 =1e27 * 200 / 10000 = 2e25(20,000,000 代币)。- Alice 持有 Agent ID,余额 0,用
100 BNB买入:- 成交到 20,000,000 代币为止;多余 BNB 退回。
buyQuotaOf(token, alice)→bought = 2e25(netBought = 2e25),remaining = 0。
- Alice 把 5,000,000 代币转到另一个钱包:
netBought仍是 20,000,000;held = max(2e25, 1.5e25) = 2e25;remaining仍为 0。- 接收方余额 5,000,000 会占用接收方自己的额度。
- Alice 把 10,000,000 代币卖回曲线:
netBought = 1e25;余额 5,000,000;held = max(1e25, 5e24) = 1e25;remaining = 1e25(还可买入 10,000,000)。她持有 5,000,000。
- Bob 买入 20,000,000 代币并全部转给 Alice:
- Alice 余额 25,000,000;
netBought10,000,000;held = 2.5e25 > cap;remaining = 0。 - Alice 卖出 25,000,000 代币:
netBought归 0,额度恢复到 2e25。 - Bob 的
netBought仍为2e25:把代币送出并不会释放他的额度。
- Alice 余额 25,000,000;
- Alice 转出 Agent ID:
- 代币仍可转账,但曲线卖出返回
NFTGatedOnly(alice)。
- 代币仍可转账,但曲线卖出返回
- 代币毕业(
LaunchedToDEX,status == 4):- Portal 交易路径关闭;在 DEX 交易,Flap 侧不再有 NFT 要求。
链上案例(BNB Chain)
| Token | NFT |
|---|---|
0xe0ec675392020EF6e8C871A9822ec50b90B37777 | 测试 Agent ID NFT:0xfDE93258a9E3896CAc41B5cf1f7530aD806961e6。合约已开源,可直接调用 mint(address to) 免费领取。 |
0x4131Cae06908e7D874cBA44004d8803045367777 | Binance Agent ID NFT:0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F。 |
9. ABI 片段
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(仅新增接口)
[
{
"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、TokenSold 均未变化,
继续使用现有 ABI。
10. 集成清单
- 用 try/catch 探测
nftGate,兼容v5.27.0之前的 Portal。 - 加载代币时读取
getTokenV8Safe.status、nftGate、maxBuyPerOrigin和buyQuotaOf(token, user)。 -
status == 1且nftGate != 0时,显示 Agent ID 提示、合集及用户持有状态。 -
nft.balanceOf(user) == 0或remaining == 0时禁用买入。 -
nft.balanceOf(user) == 0时禁用卖出;卖出不受 netBought 上限限制。 - 报价时传
from = user以得到准确的上限结果;记住报价不检查 NFT。 - 门控交易由 EOA 直接发送,避免中继器和 4337 bundler。
- 处理
NFTGatedOnly、BuyCapExceeded、SlippageTooHigh和TokenNotTradable。ABI 继续使用NFT旧名,Agent ID 是产品术语。 - 展示实时上限进度(
bought / cap、remaining),曲线买入、卖出和转账后刷新。 - 发射工具在提交前检查
nftWhitelisted(nft)(Agent ID 合集);处理NFTNotWhitelisted。 - 监听
FlapNFTWhitelistUpdated。 - 收到
LaunchedToDEX或status == 4后切换到 DEX 路由并移除门控 UI。 - 索引
FlapNFTGated,在用户打开代币前就知道它是否 Agent ID 门控;并索引FlapNFTGateUpdated, 在 admin 移除门控后撤下 Agent ID UI。
11. 合约地址
| 链 | Chain ID | Portal 代理 |
|---|---|---|
| BNB Smart Chain 主网 | 56 | 0xe2cE6ab80874Fa9Fa2aAE65D277Dd6B8e65C9De0 |
| BNB Smart Chain 测试网 | 97 | 0x5bEacaF7ABCbB3aB280e80D007FD31fcE26510e9 |
其他链使用不同的 Portal 代理,请从 Flap 的已部署合约地址页面获取。各链的 ABI 与本文语义完全一致。