skills/bap578-scanner/SKILL.md
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.
npx skillsauth add chatandbuild/skills-repo BAP-578 On-Chain ScannerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill to read, verify, and index BAP-578 agent data directly from the blockchain. This covers single-agent lookups, bulk scanning, event indexing, vault integrity checks, and building automated monitoring pipelines.
The scanner reveals the full on-chain identity of any agent. By calling getAgentState and getAgentMetadata, you retrieve the complete profile:
This is the authoritative identity. Any off-chain representation should match these fields.
The scanner can reconstruct the complete history of an agent by querying on-chain events:
Additionally, the scanner can verify extended memory by fetching vaultURI content and comparing its keccak256 hash against the on-chain vaultHash. This proves whether the off-chain data is authentic.
The scanner performs read-only operations. It does not modify state. Capabilities include:
Scanner results come directly from the blockchain via RPC calls. Trust is established by:
getAgentState(tokenId) → (balance, active, logicAddress, createdAt, owner)
getAgentMetadata(tokenId) → (persona, experience, voiceHash, animationURI, vaultURI, vaultHash)
tokensOfOwner(address) → uint256[]
getTotalSupply() → uint256
getFreeMints(user) → uint256
isFreeMint(tokenId) → bool
tokenURI(tokenId) → string
ownerOf(tokenId) → address
AgentCreated(tokenId, owner, logicAddress, metadataURI)
AgentFunded(tokenId, funder, amount)
AgentWithdraw(tokenId, owner, amount)
AgentStatusChanged(tokenId, active)
MetadataUpdated(tokenId, newURI)
Transfer(from, to, tokenId) // ERC-721 standard
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");
const contract = new ethers.Contract(BAP578_ADDRESS, BAP578_ABI, provider);
async function scanAgent(tokenId) {
const state = await contract.getAgentState(tokenId);
const metadata = await contract.getAgentMetadata(tokenId);
const uri = await contract.tokenURI(tokenId);
const freeMint = await contract.isFreeMint(tokenId);
return {
tokenId,
owner: state.owner,
balance: ethers.formatEther(state.balance),
active: state.active,
logicAddress: state.logicAddress,
createdAt: new Date(Number(state.createdAt) * 1000).toISOString(),
persona: metadata.persona,
experience: metadata.experience,
voiceHash: metadata.voiceHash,
animationURI: metadata.animationURI,
vaultURI: metadata.vaultURI,
vaultHash: metadata.vaultHash,
tokenURI: uri,
isFreeMint: freeMint,
};
}
async function scanOwnerPortfolio(ownerAddress) {
const tokenIds = await contract.tokensOfOwner(ownerAddress);
const agents = [];
for (const id of tokenIds) {
agents.push(await scanAgent(id));
}
return { owner: ownerAddress, agentCount: agents.length, agents };
}
async function scanAllAgents() {
const totalSupply = await contract.getTotalSupply();
const agents = [];
for (let i = 1; i <= Number(totalSupply); i++) {
try {
agents.push(await scanAgent(i));
} catch (e) {
console.warn(`Token ${i} not found or burned`);
}
}
return agents;
}
async function computeMetrics() {
const agents = await scanAllAgents();
const totalSupply = agents.length;
const activeCount = agents.filter((a) => a.active).length;
const uniqueOwners = new Set(agents.map((a) => a.owner)).size;
const totalBalance = agents.reduce(
(sum, a) => sum + parseFloat(a.balance),
0
);
const freeMintCount = agents.filter((a) => a.isFreeMint).length;
const paidMintCount = totalSupply - freeMintCount;
const withLogic = agents.filter(
(a) => a.logicAddress !== ethers.ZeroAddress
).length;
return {
totalSupply,
activeCount,
inactiveCount: totalSupply - activeCount,
uniqueOwners,
tvl: totalBalance.toFixed(4) + " BNB",
freeMintCount,
paidMintCount,
agentsWithLogic: withLogic,
};
}
import { createPublicClient, http } from "viem";
import { bsc } from "viem/chains";
const client = createPublicClient({
chain: bsc,
transport: http(process.env.BSC_RPC_URL),
});
const state = await client.readContract({
address: BAP578_ADDRESS,
abi: BAP578_ABI,
functionName: "getAgentState",
args: [tokenId],
});
const events = await client.getContractEvents({
address: BAP578_ADDRESS,
abi: BAP578_ABI,
eventName: "AgentCreated",
fromBlock: deploymentBlock,
toBlock: "latest",
});
The vault is the off-chain memory layer. Verification ensures it has not been tampered with.
const { keccak256, toUtf8Bytes } = require("ethers");
async function verifyVault(vaultURI, onChainHash) {
if (onChainHash === ethers.ZeroHash) {
return { verified: false, reason: "No vault hash registered (zero hash)" };
}
try {
const response = await fetch(vaultURI);
if (!response.ok) {
return { verified: false, reason: `Fetch failed: ${response.status}` };
}
const content = await response.text();
const computedHash = keccak256(toUtf8Bytes(content));
if (computedHash === onChainHash) {
return { verified: true, contentLength: content.length };
} else {
return {
verified: false,
reason: "Hash mismatch — content may have been modified",
expected: onChainHash,
computed: computedHash,
};
}
} catch (error) {
return { verified: false, reason: `Error: ${error.message}` };
}
}
async function indexAllEvents(fromBlock = 0n) {
const eventNames = [
"AgentCreated",
"AgentFunded",
"AgentWithdraw",
"AgentStatusChanged",
"MetadataUpdated",
];
const allEvents = [];
for (const eventName of eventNames) {
const events = await contract.queryFilter(
contract.filters[eventName](),
fromBlock,
"latest"
);
for (const event of events) {
allEvents.push({
eventName,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
args: event.args,
timestamp: null, // fetch block timestamp if needed
});
}
}
// Sort by block number
allEvents.sort((a, b) => a.blockNumber - b.blockNumber);
return allEvents;
}
async function getAgentHistory(tokenId) {
const allEvents = await indexAllEvents();
return allEvents.filter((e) => {
const args = e.args;
return args.tokenId && args.tokenId.toString() === tokenId.toString();
});
}
function watchAgentEvents() {
contract.on("AgentCreated", (tokenId, owner, logicAddress, metadataURI) => {
console.log(`New agent #${tokenId} minted by ${owner}`);
});
contract.on("AgentFunded", (tokenId, funder, amount) => {
console.log(
`Agent #${tokenId} funded ${ethers.formatEther(amount)} BNB by ${funder}`
);
});
contract.on("AgentWithdraw", (tokenId, owner, amount) => {
console.log(
`Agent #${tokenId} withdrew ${ethers.formatEther(amount)} BNB`
);
});
contract.on("AgentStatusChanged", (tokenId, active) => {
console.log(`Agent #${tokenId} status → ${active ? "active" : "inactive"}`);
});
contract.on("MetadataUpdated", (tokenId, newURI) => {
console.log(`Agent #${tokenId} metadata updated → ${newURI}`);
});
}
try {
await contract.ownerOf(tokenId);
// Token exists
} catch {
// Token does not exist or was burned
}
const agents = await scanAllAgents();
const withLogic = agents.filter(
(a) => a.logicAddress !== ethers.ZeroAddress
);
const atRisk = agents.filter(
(a) => !a.active && parseFloat(a.balance) > 0
);
const recentEvents = await contract.queryFilter(
contract.filters.AgentCreated(),
-1000 // last 1000 blocks
);
When asked for scanning help, respond with:
{
"tokenId": 17,
"owner": "0xABC...",
"balance": "1.5",
"balanceUnit": "BNB",
"active": true,
"logicAddress": "0xDEF...",
"createdAt": "2026-03-01T10:00:00Z",
"persona": {"name": "Atlas", "traits": ["analytical"]},
"experience": "Financial analyst specializing in DeFi",
"voiceHash": "",
"animationURI": "ipfs://QmAnim...",
"vaultURI": "ipfs://QmVault...",
"vaultHash": "0xabc123...",
"vaultVerified": true,
"isFreeMint": false,
"tokenURI": "ipfs://QmMeta...",
"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..."}
]
}
tokenId,owner,balance,active,logicAddress,createdAt,experience,isFreeMint
1,0xABC...,0.5,true,0x0000...,2026-03-01,DeFi analyst,true
2,0xDEF...,1.2,true,0xLOGIC...,2026-03-02,Research bot,false
3,0x123...,0.0,false,0x0000...,2026-03-03,Support agent,true
function toCSV(agents) {
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}`
);
return [header, ...rows].join("\n");
}
Detect whether a small number of addresses control most agents:
function analyzeConcentration(agents) {
const ownerCounts = {};
for (const a of agents) {
ownerCounts[a.owner] = (ownerCounts[a.owner] || 0) + 1;
}
const sorted = Object.entries(ownerCounts)
.sort(([,a], [,b]) => b - a);
const totalAgents = agents.length;
const top10 = sorted.slice(0, 10);
const top10Count = top10.reduce((sum, [, c]) => sum + c, 0);
return {
totalOwners: sorted.length,
top10Owners: top10.map(([addr, count]) => ({
address: addr,
count,
percentage: ((count / totalAgents) * 100).toFixed(1) + "%"
})),
top10Concentration: ((top10Count / totalAgents) * 100).toFixed(1) + "%"
};
}
Assign a health score to each agent based on multiple factors:
function scoreAgentHealth(agent) {
let score = 0;
// Active status (30 points)
if (agent.active) score += 30;
// Has balance (20 points, scaled)
const balance = parseFloat(agent.balance);
if (balance > 0) score += Math.min(20, balance * 10);
// Has persona (15 points)
try {
const persona = JSON.parse(agent.persona);
if (persona.name) score += 10;
if (persona.traits && persona.traits.length > 0) score += 5;
} catch {}
// Has experience (10 points)
if (agent.experience && agent.experience.length > 10) score += 10;
// Has logic contract (15 points)
if (agent.logicAddress !== ethers.ZeroAddress) score += 15;
// Has vault (10 points)
if (agent.vaultURI && agent.vaultURI.length > 0) score += 5;
if (agent.vaultHash !== ethers.ZeroHash) score += 5;
return { tokenId: agent.tokenId, score, maxScore: 100 };
}
Track how metrics change over time by bucketing events:
function mintTimeSeries(events, interval = "day") {
const buckets = {};
const created = events.filter(e => e.name === "AgentCreated");
for (const event of created) {
const date = new Date(event.timestamp);
let key;
if (interval === "day") key = date.toISOString().split("T")[0];
else if (interval === "week") {
const weekStart = new Date(date);
weekStart.setDate(date.getDate() - date.getDay());
key = weekStart.toISOString().split("T")[0];
} else if (interval === "month") {
key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
}
buckets[key] = (buckets[key] || 0) + 1;
}
return Object.entries(buckets)
.sort(([a], [b]) => a.localeCompare(b))
.map(([period, count]) => ({ period, count }));
}
Track net funding per agent over time:
function fundingFlow(events) {
const flows = {};
for (const event of events) {
if (event.name === "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") {
const id = event.args.tokenId;
flows[id] = flows[id] || { inflow: 0, outflow: 0 };
flows[id].outflow += parseFloat(event.args.amount);
}
}
return Object.entries(flows).map(([tokenId, flow]) => ({
tokenId,
inflow: flow.inflow.toFixed(4) + " BNB",
outflow: flow.outflow.toFixed(4) + " BNB",
netFlow: (flow.inflow - flow.outflow).toFixed(4) + " BNB"
}));
}
getLogs block ranges. Paginate event queries in chunks of 5000-10000 blocks.Promise.all for independent agent lookups, but respect rate limits.ethers.JsonRpcProvider.send("eth_call", [...]) with multicall for efficient bulk reads.bap578bap578-analyticsfrontend-web3-bap578tools
Use only when the user explicitly asks to stage, commit, push, and open a GitHub pull request in one flow using the GitHub CLI (`gh`).
development
Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like "the xlsx in my downloads") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved.
development
Use this skill when turning messy workout information into clear logs, comparing user-provided sessions, surfacing trends or likely PRs, and suggesting realistic next-session steps.
tools
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.