From fed10cfe8fd1f10aaf1de2c16dcca0cddac390c3 Mon Sep 17 00:00:00 2001 From: BORT AI Date: Tue, 4 Aug 2026 14:33:40 +0300 Subject: [PATCH 1/5] fix(bap578-scanner): correct ABI shapes and add spec/reference compatibility Testing the scanner against a live BAP-578 collection surfaced a few signatures that do not match BAP578.sol: - getAgentMetadata returns (AgentMetadata, string), not six flat values; the scanAgent example read metadata.persona off the wrong level - AgentFunded / AgentWithdraw / MetadataUpdated were documented with extra params (funder, owner, newURI) the contract does not emit - LogicAddressUpdated was missing Also adds a Deployment compatibility section: some deployments implement the BAP-578 spec shape (getState / State / Status enum) rather than the reference, so the scanner shows a getState-then-getAgentState fallback and notes the Status enum ordinal is deployment-specific. Verified against BAP578.sol on main and a live mainnet collection. --- skills/bap578-scanner/SKILL.md | 87 +++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/skills/bap578-scanner/SKILL.md b/skills/bap578-scanner/SKILL.md index 55394549..514aa577 100644 --- a/skills/bap578-scanner/SKILL.md +++ b/skills/bap578-scanner/SKILL.md @@ -4,7 +4,7 @@ name: BAP-578 On-Chain Scanner description: Use this skill when reading, verifying, scanning, querying, indexing, or monitoring BAP-578 agent data directly from BNB Chain, including metadata, event history, vault integrity, and bulk RPC workflows. category: Blockchain author: community -version: 1.0.0 +version: 1.1.0 --- # BAP-578 On-Chain Scanner @@ -46,7 +46,7 @@ This is the authoritative identity. Any off-chain representation should match th The scanner can reconstruct the complete history of an agent by querying on-chain events: - **AgentCreated** — when and how the agent was born (minter, initial metadata) -- **AgentFunded** — every BNB deposit with amount and sender +- **AgentFunded** — every BNB deposit with amount - **AgentWithdraw** — every withdrawal with amount - **AgentStatusChanged** — every active/inactive toggle - **MetadataUpdated** — every identity change (new persona, vault, etc.) @@ -84,7 +84,7 @@ Scanner results come directly from the blockchain via RPC calls. Trust is establ ``` getAgentState(tokenId) → (balance, active, logicAddress, createdAt, owner) -getAgentMetadata(tokenId) → (persona, experience, voiceHash, animationURI, vaultURI, vaultHash) +getAgentMetadata(tokenId) → (AgentMetadata metadata, string metadataURI) // metadata: persona, experience, voiceHash, animationURI, vaultURI, vaultHash tokensOfOwner(address) → uint256[] getTotalSupply() → uint256 getFreeMints(user) → uint256 @@ -97,13 +97,69 @@ ownerOf(tokenId) → address ``` AgentCreated(tokenId, owner, logicAddress, metadataURI) -AgentFunded(tokenId, funder, amount) -AgentWithdraw(tokenId, owner, amount) +AgentFunded(tokenId, amount) +AgentWithdraw(tokenId, amount) AgentStatusChanged(tokenId, active) -MetadataUpdated(tokenId, newURI) +LogicAddressUpdated(tokenId, newLogicAddress) +MetadataUpdated(tokenId) Transfer(from, to, tokenId) // ERC-721 standard ``` +## Deployment compatibility (spec vs reference) + +The signatures above match the ChatAndBuild reference implementation +(`getAgentState`, `AgentStatusChanged(tokenId, bool active)`). Some +production deployments implement the **BAP-578 spec** shape instead, which +is not call-compatible: + +```solidity +enum Status { Active, Paused, Terminated } +struct State { + uint256 balance; + Status status; + address owner; + address logicAddress; + uint256 lastActionTimestamp; +} +function getState(uint256 tokenId) external view returns (State memory); +event StatusChanged(address indexed agent, Status newStatus); +``` + +Try `getState` first and fall back to +`getAgentState`, so it works against both shapes: + +```js +async function readState(contract, tokenId) { + try { + const s = await contract.getState(tokenId); // spec shape + return { + balance: s.balance, + owner: s.owner, + logicAddress: s.logicAddress, + status: s.status, + lastActionTimestamp: s.lastActionTimestamp, + shape: "spec", + }; + } catch { + const s = await contract.getAgentState(tokenId); // reference shape + return { + balance: s.balance, + active: s.active, + owner: s.owner, + logicAddress: s.logicAddress, + createdAt: s.createdAt, + shape: "reference", + }; + } +} +``` + +**Status enum ordinal is deployment-specific.** The spec declares +`Active = 0`, but a deployment may differ (for example a live collection +that ships `Paused = 0, Active = 1, Terminated = 2`). This is fixed at +deploy time and cannot change for already-minted agents, so always map the +integer using the target deployment's verified enum, never a hard-coded array. + --- ## Scanning with ethers.js @@ -124,9 +180,8 @@ const contract = new ethers.Contract(BAP578_ADDRESS, BAP578_ABI, provider); ```js async function scanAgent(tokenId) { - const state = await contract.getAgentState(tokenId); - const metadata = await contract.getAgentMetadata(tokenId); - const uri = await contract.tokenURI(tokenId); + const state = await contract.getAgentState(tokenId); // spec-shape deployments: use readState() from "Deployment compatibility" + const [metadata, metadataURI] = await contract.getAgentMetadata(tokenId); const freeMint = await contract.isFreeMint(tokenId); return { @@ -142,7 +197,7 @@ async function scanAgent(tokenId) { animationURI: metadata.animationURI, vaultURI: metadata.vaultURI, vaultHash: metadata.vaultHash, - tokenURI: uri, + tokenURI: metadataURI, isFreeMint: freeMint, }; } @@ -358,13 +413,13 @@ function watchAgentEvents() { console.log(`New agent #${tokenId} minted by ${owner}`); }); - contract.on("AgentFunded", (tokenId, funder, amount) => { + contract.on("AgentFunded", (tokenId, amount) => { console.log( - `Agent #${tokenId} funded ${ethers.formatEther(amount)} BNB by ${funder}` + `Agent #${tokenId} funded ${ethers.formatEther(amount)} BNB` ); }); - contract.on("AgentWithdraw", (tokenId, owner, amount) => { + contract.on("AgentWithdraw", (tokenId, amount) => { console.log( `Agent #${tokenId} withdrew ${ethers.formatEther(amount)} BNB` ); @@ -374,8 +429,8 @@ function watchAgentEvents() { console.log(`Agent #${tokenId} status → ${active ? "active" : "inactive"}`); }); - contract.on("MetadataUpdated", (tokenId, newURI) => { - console.log(`Agent #${tokenId} metadata updated → ${newURI}`); + contract.on("MetadataUpdated", (tokenId) => { + console.log(`Agent #${tokenId} metadata updated`); }); } ``` @@ -460,7 +515,7 @@ When asked for scanning help, respond with: "history": [ {"event": "AgentCreated", "block": 12345, "tx": "0x...", "timestamp": "2026-03-01T10:00:00Z"}, {"event": "AgentFunded", "block": 12400, "tx": "0x...", "amount": "1.0 BNB"}, - {"event": "MetadataUpdated", "block": 12500, "tx": "0x...", "newURI": "ipfs://QmNew..."} + {"event": "MetadataUpdated", "block": 12500, "tx": "0x..."} ] } ``` From 7974bbd633c7e5dc165a308f105cd4f9b682ccea Mon Sep 17 00:00:00 2001 From: BORT AI Date: Tue, 4 Aug 2026 14:43:31 +0300 Subject: [PATCH 2/5] fix(bap578-scanner): route scanAgent through readState, guard isFreeMint Review feedback: scanAgent still called the reference-only getAgentState directly, so it failed on the spec-shape deployments the compatibility section documents. It now reads state via readState (spec first, reference fallback), reports which shape answered, and returns the union of both shapes' fields. isFreeMint is reference-only, so it is wrapped in a try/catch and reported as undefined on spec deployments instead of aborting the scan. --- skills/bap578-scanner/SKILL.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/skills/bap578-scanner/SKILL.md b/skills/bap578-scanner/SKILL.md index 514aa577..f16adc05 100644 --- a/skills/bap578-scanner/SKILL.md +++ b/skills/bap578-scanner/SKILL.md @@ -180,17 +180,28 @@ const contract = new ethers.Contract(BAP578_ADDRESS, BAP578_ABI, provider); ```js async function scanAgent(tokenId) { - const state = await contract.getAgentState(tokenId); // spec-shape deployments: use readState() from "Deployment compatibility" + const state = await readState(contract, tokenId); // works on spec + reference shapes const [metadata, metadataURI] = await contract.getAgentMetadata(tokenId); - const freeMint = await contract.isFreeMint(tokenId); + // isFreeMint is reference-only; it reverts on spec-shape deployments. + let freeMint; + try { + freeMint = await contract.isFreeMint(tokenId); + } catch { + freeMint = undefined; + } return { tokenId, + shape: state.shape, owner: state.owner, balance: ethers.formatEther(state.balance), - active: state.active, + active: state.active, // reference shape + status: state.status, // spec shape logicAddress: state.logicAddress, - createdAt: new Date(Number(state.createdAt) * 1000).toISOString(), + createdAt: state.createdAt + ? new Date(Number(state.createdAt) * 1000).toISOString() + : undefined, + lastActionTimestamp: state.lastActionTimestamp, // spec shape persona: metadata.persona, experience: metadata.experience, voiceHash: metadata.voiceHash, From 6d2b72792170be0a92377f3b3e3bee8a1574f50e Mon Sep 17 00:00:00 2001 From: BORT AI Date: Tue, 4 Aug 2026 14:50:07 +0300 Subject: [PATCH 3/5] docs(bap578-scanner): require both getters in the ABI for the fallback Review feedback: the getState/getAgentState fallback silently fails when the contract instance is built from the reference ABI alone. contract getState is undefined in that case, so the call throws a TypeError before reaching the chain and the fallback then reverts on a spec deployment. Documents the combined ABI and shows the spec fragment to append. --- skills/bap578-scanner/SKILL.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/skills/bap578-scanner/SKILL.md b/skills/bap578-scanner/SKILL.md index f16adc05..37512493 100644 --- a/skills/bap578-scanner/SKILL.md +++ b/skills/bap578-scanner/SKILL.md @@ -125,8 +125,20 @@ function getState(uint256 tokenId) external view returns (State memory); event StatusChanged(address indexed agent, Status newStatus); ``` -Try `getState` first and fall back to -`getAgentState`, so it works against both shapes: +The contract instance must be built with an ABI that carries **both** +getters. If the ABI only has the reference shape, `contract.getState` is +undefined and the call throws before it ever reaches the chain, so add the +spec fragments alongside the reference ones: + +```js +const BAP578_ABI = [ + ...require("./abi/BAP578.json"), // reference shape + // spec shape + "function getState(uint256 tokenId) view returns (tuple(uint256 balance, uint8 status, address owner, address logicAddress, uint256 lastActionTimestamp))", +]; +``` + +Then try `getState` first and fall back to `getAgentState`: ```js async function readState(contract, tokenId) { From 9fa4abe8c53e9fedbb4ef7a022b9acd2ebc9de83 Mon Sep 17 00:00:00 2001 From: BORT AI Date: Tue, 4 Aug 2026 15:03:25 +0300 Subject: [PATCH 4/5] fix(bap578-scanner): make every documented flow work on both shapes Follows the review thread to its conclusion. getAgentMetadata shares a selector across shapes but returns different values, so it is now decoded from the raw call data with a reference-then-spec fallback (readMetadata); decoding a spec return with the reference shape throws, and the bulk scanner was swallowing that as token-not-found and dropping valid agents. scanAllAgents falls back from getTotalSupply to ERC-721 totalSupply, and scanOwnerPortfolio falls back from tokensOfOwner to balanceOf plus tokenOfOwnerByIndex, so both entry points run on spec deployments. Liveness is resolved through one isActive(agent, activeStatus) helper used by computeMetrics, scoreAgentHealth, the at-risk pattern and the CSV export, instead of reading the reference-only active flag. The viem example and event-indexing sections remain reference-only by design: spec deployments emit different event signatures, which the compatibility section now states explicitly. --- skills/bap578-scanner/SKILL.md | 93 ++++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/skills/bap578-scanner/SKILL.md b/skills/bap578-scanner/SKILL.md index 37512493..a4501dca 100644 --- a/skills/bap578-scanner/SKILL.md +++ b/skills/bap578-scanner/SKILL.md @@ -28,7 +28,7 @@ Use this skill to read, verify, and index BAP-578 agent data directly from the b ### 1) Who are you? -The scanner reveals the full on-chain identity of any agent. By calling `getAgentState` and `getAgentMetadata`, you retrieve the complete profile: +The scanner reveals the full on-chain identity of any agent. By reading the state and metadata getters (see Deployment compatibility for the two shapes), you retrieve the complete profile: - Token ID (the unique agent identifier) - Owner address (who controls this agent) @@ -166,12 +166,60 @@ async function readState(contract, tokenId) { } ``` +`getAgentMetadata` needs the same treatment, but it cannot be solved with an +ABI entry: both shapes share the selector `getAgentMetadata(uint256)` and +differ only in their return values, so decode the raw result explicitly. +Decoding a spec return with the reference shape throws, and a bulk scan that +catches per-token errors will silently drop those agents: + +```js +const META = + "tuple(string persona, string experience, string voiceHash, string animationURI, string vaultURI, bytes32 vaultHash)"; + +async function readMetadata(provider, address, tokenId) { + const iface = new ethers.Interface([ + `function getAgentMetadata(uint256) view returns (${META} metadata, string metadataURI)`, + ]); + const raw = await provider.call({ + to: address, + data: iface.encodeFunctionData("getAgentMetadata", [tokenId]), + }); + const coder = ethers.AbiCoder.defaultAbiCoder(); + try { + const [metadata, metadataURI] = coder.decode([META, "string"], raw); // reference + return { metadata, metadataURI, shape: "reference" }; + } catch { + const [metadata] = coder.decode([META], raw); // spec + return { metadata, metadataURI: undefined, shape: "spec" }; + } +} +``` + +The two shapes also report liveness differently: reference has a boolean +`active`, spec has a `status` enum whose ordinal is deployment-specific. +Resolve it once and reuse it wherever agents are filtered or scored: + +```js +// activeStatus is the integer this deployment uses for Active. +function isActive(agent, activeStatus) { + return agent.shape === "spec" + ? Number(agent.status) === activeStatus + : agent.active; +} +``` + **Status enum ordinal is deployment-specific.** The spec declares `Active = 0`, but a deployment may differ (for example a live collection that ships `Paused = 0, Active = 1, Terminated = 2`). This is fixed at deploy time and cannot change for already-minted agents, so always map the integer using the target deployment's verified enum, never a hard-coded array. +The viem example and the event-indexing sections below document the +reference implementation only. Spec deployments emit different event +signatures (`StatusChanged(address, Status)`, `AgentFunded(address, address, +uint256)`) and no `AgentCreated`, so an indexer for a spec deployment needs +its own event map taken from that deployment's verified ABI. + --- ## Scanning with ethers.js @@ -193,7 +241,11 @@ const contract = new ethers.Contract(BAP578_ADDRESS, BAP578_ABI, provider); ```js async function scanAgent(tokenId) { const state = await readState(contract, tokenId); // works on spec + reference shapes - const [metadata, metadataURI] = await contract.getAgentMetadata(tokenId); + const { metadata, metadataURI } = await readMetadata( + provider, + BAP578_ADDRESS, + tokenId, + ); // works on spec + reference shapes // isFreeMint is reference-only; it reverts on spec-shape deployments. let freeMint; try { @@ -230,7 +282,17 @@ async function scanAgent(tokenId) { ```js async function scanOwnerPortfolio(ownerAddress) { - const tokenIds = await contract.tokensOfOwner(ownerAddress); + // tokensOfOwner is reference-only; fall back to ERC-721 Enumerable. + let tokenIds; + try { + tokenIds = await contract.tokensOfOwner(ownerAddress); + } catch { + const count = Number(await contract.balanceOf(ownerAddress)); + tokenIds = []; + for (let i = 0; i < count; i++) { + tokenIds.push(await contract.tokenOfOwnerByIndex(ownerAddress, i)); + } + } const agents = []; for (const id of tokenIds) { agents.push(await scanAgent(id)); @@ -243,7 +305,14 @@ async function scanOwnerPortfolio(ownerAddress) { ```js async function scanAllAgents() { - const totalSupply = await contract.getTotalSupply(); + // getTotalSupply is reference-only; spec deployments expose ERC-721 + // Enumerable totalSupply instead. + let totalSupply; + try { + totalSupply = await contract.getTotalSupply(); + } catch { + totalSupply = await contract.totalSupply(); + } const agents = []; for (let i = 1; i <= Number(totalSupply); i++) { try { @@ -259,10 +328,11 @@ async function scanAllAgents() { ### Aggregate metrics ```js -async function computeMetrics() { +async function computeMetrics(activeStatus) { const agents = await scanAllAgents(); const totalSupply = agents.length; - const activeCount = agents.filter((a) => a.active).length; + // Pass the integer this deployment uses for Active (see isActive above). + const activeCount = agents.filter((a) => isActive(a, activeStatus)).length; const uniqueOwners = new Set(agents.map((a) => a.owner)).size; const totalBalance = agents.reduce( (sum, a) => sum + parseFloat(a.balance), @@ -486,7 +556,7 @@ const withLogic = agents.filter( ```js const atRisk = agents.filter( - (a) => !a.active && parseFloat(a.balance) > 0 + (a) => !isActive(a, activeStatus) && parseFloat(a.balance) > 0, ); ``` @@ -520,6 +590,7 @@ When asked for scanning help, respond with: ```json { "tokenId": 17, + "shape": "reference", "owner": "0xABC...", "balance": "1.5", "balanceUnit": "BNB", @@ -555,10 +626,10 @@ tokenId,owner,balance,active,logicAddress,createdAt,experience,isFreeMint ### Generating CSV from scan results ```js -function toCSV(agents) { +function toCSV(agents, activeStatus) { const header = "tokenId,owner,balance,active,logicAddress,createdAt,experience,isFreeMint"; const rows = agents.map(a => - `${a.tokenId},${a.owner},${a.balance},${a.active},${a.logicAddress},${a.createdAt},${a.experience.replace(/,/g, ";")},${a.isFreeMint}` + `${a.tokenId},${a.owner},${a.balance},${isActive(a, activeStatus)},${a.logicAddress},${a.createdAt ?? ""},${a.experience.replace(/,/g, ";")},${a.isFreeMint ?? ""}` ); return [header, ...rows].join("\n"); } @@ -603,11 +674,11 @@ function analyzeConcentration(agents) { Assign a health score to each agent based on multiple factors: ```js -function scoreAgentHealth(agent) { +function scoreAgentHealth(agent, activeStatus) { let score = 0; // Active status (30 points) - if (agent.active) score += 30; + if (isActive(agent, activeStatus)) score += 30; // Has balance (20 points, scaled) const balance = parseFloat(agent.balance); From 9ae3c1a16329f1e7ff26c0679125c29afc7fc1aa Mon Sep 17 00:00:00 2001 From: BORT AI Date: Tue, 4 Aug 2026 15:10:34 +0300 Subject: [PATCH 5/5] fix(bap578-scanner): build the combined ABI in Setup, mark reference-only getters The compatibility section described the combined ABI but the Setup block still constructed the contract from the reference ABI alone, so contract.getState was undefined and every fallback threw before reaching the chain. Setup is now the single source and also carries the enumerable fragments the supply and portfolio fallbacks call. The view-function list marks which getters are reference-only and which return a different shape under the spec, so the divergences are visible where the functions are introduced rather than only in the compatibility section. Also corrects a pre-existing mismatch: indexAllEvents emits eventName while mintTimeSeries and fundingFlow read event.name, so both silently returned empty results. --- skills/bap578-scanner/SKILL.md | 50 ++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/skills/bap578-scanner/SKILL.md b/skills/bap578-scanner/SKILL.md index a4501dca..3163ac2b 100644 --- a/skills/bap578-scanner/SKILL.md +++ b/skills/bap578-scanner/SKILL.md @@ -83,12 +83,12 @@ Scanner results come directly from the blockchain via RPC calls. Trust is establ ### View Functions Used ``` -getAgentState(tokenId) → (balance, active, logicAddress, createdAt, owner) -getAgentMetadata(tokenId) → (AgentMetadata metadata, string metadataURI) // metadata: persona, experience, voiceHash, animationURI, vaultURI, vaultHash -tokensOfOwner(address) → uint256[] -getTotalSupply() → uint256 -getFreeMints(user) → uint256 -isFreeMint(tokenId) → bool +getAgentState(tokenId) → (balance, active, logicAddress, createdAt, owner) // reference-only, spec uses getState +getAgentMetadata(tokenId) → (AgentMetadata metadata, string metadataURI) // spec returns the tuple alone +tokensOfOwner(address) → uint256[] // reference-only +getTotalSupply() → uint256 // reference-only +getFreeMints(user) → uint256 // reference-only +isFreeMint(tokenId) → bool // reference-only tokenURI(tokenId) → string ownerOf(tokenId) → address ``` @@ -125,20 +125,14 @@ function getState(uint256 tokenId) external view returns (State memory); event StatusChanged(address indexed agent, Status newStatus); ``` -The contract instance must be built with an ABI that carries **both** -getters. If the ABI only has the reference shape, `contract.getState` is -undefined and the call throws before it ever reaches the chain, so add the -spec fragments alongside the reference ones: +The contract instance must carry both getters, which is what the Setup +section does. An ABI with only the reference shape leaves +`contract.getState` undefined, so the call throws before it reaches the +chain and the fallback then reverts on a spec deployment. The same applies +to the enumerable fragments the supply and portfolio fallbacks use. -```js -const BAP578_ABI = [ - ...require("./abi/BAP578.json"), // reference shape - // spec shape - "function getState(uint256 tokenId) view returns (tuple(uint256 balance, uint8 status, address owner, address logicAddress, uint256 lastActionTimestamp))", -]; -``` - -Then try `getState` first and fall back to `getAgentState`: +With that ABI in place, try `getState` first and fall back to +`getAgentState`: ```js async function readState(contract, tokenId) { @@ -231,7 +225,17 @@ const { ethers } = require("ethers"); const provider = new ethers.JsonRpcProvider(process.env.BSC_RPC_URL); const BAP578_ADDRESS = process.env.BAP578_ADDRESS; -const BAP578_ABI = require("./abi/BAP578.json"); + +// The reference ABI plus every fragment the fallbacks call, so one contract +// instance works against both deployment shapes. Without these the fallback +// paths throw before reaching the chain. See Deployment compatibility. +const BAP578_ABI = [ + ...require("./abi/BAP578.json"), // reference shape + "function getState(uint256 tokenId) view returns (tuple(uint256 balance, uint8 status, address owner, address logicAddress, uint256 lastActionTimestamp))", + "function totalSupply() view returns (uint256)", + "function balanceOf(address owner) view returns (uint256)", + "function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)", +]; const contract = new ethers.Contract(BAP578_ADDRESS, BAP578_ABI, provider); ``` @@ -712,7 +716,7 @@ Track how metrics change over time by bucketing events: ```js function mintTimeSeries(events, interval = "day") { const buckets = {}; - const created = events.filter(e => e.name === "AgentCreated"); + const created = events.filter(e => e.eventName === "AgentCreated"); for (const event of created) { const date = new Date(event.timestamp); @@ -743,11 +747,11 @@ function fundingFlow(events) { const flows = {}; for (const event of events) { - if (event.name === "AgentFunded") { + if (event.eventName === "AgentFunded") { const id = event.args.tokenId; flows[id] = flows[id] || { inflow: 0, outflow: 0 }; flows[id].inflow += parseFloat(event.args.amount); - } else if (event.name === "AgentWithdraw") { + } else if (event.eventName === "AgentWithdraw") { const id = event.args.tokenId; flows[id] = flows[id] || { inflow: 0, outflow: 0 }; flows[id].outflow += parseFloat(event.args.amount);