BAP-578 On-Chain Scanner
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.
When to use this skill
- Looking up a specific agent's identity, state, or metadata by token ID.
- Listing all agents owned by a particular address.
- Verifying that off-chain vault content matches the on-chain hash.
- Building an event indexer or monitoring pipeline for BAP-578.
- Auditing agent histories through event reconstruction.
- Answering questions like "how many agents exist?" or "what's the TVL?"
The Four Identity Questions (Scanner View)
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:
- Token ID (the unique agent identifier)
- Owner address (who controls this agent)
- Persona (JSON-encoded character definition)
- Experience (plain-text domain expertise)
- Logic address (optional behavior contract)
- Active status (operational or dormant)
- Creation timestamp (when the agent was minted)
- Balance (BNB held by the agent)
This is the authoritative identity. Any off-chain representation should match these fields.
2) What do you remember?
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
- AgentWithdraw — every withdrawal with amount
- AgentStatusChanged — every active/inactive toggle
- MetadataUpdated — every identity change (new persona, vault, etc.)
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.
3) What can you do?
The scanner performs read-only operations. It does not modify state. Capabilities include:
- Single agent lookup (state + metadata)
- Owner portfolio listing (all tokens owned by an address)
- Total supply query
- Free mint status check
- Event history reconstruction
- Vault content verification
- Bulk scanning across token ID ranges
- Aggregate metrics computation (TVL, active count, mint velocity)
4) How can I trust it?
Scanner results come directly from the blockchain via RPC calls. Trust is established by:
- Reading from the canonical contract address (verified on block explorer)
- Using authenticated RPC endpoints
- Cross-referencing event data with state reads
- Verifying vault hashes independently
- All data is publicly verifiable by anyone with an RPC connection
Contract Interface for Scanning
View Functions Used
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
Events Indexed
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
Scanning with ethers.js
Setup
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);
Single agent lookup
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,
};
}
Owner portfolio scan
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 };
}
Bulk scan (all 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;
}
Aggregate metrics
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,
};
}
Scanning with viem
Setup
import { createPublicClient, http } from "viem";
import { bsc } from "viem/chains";
const client = createPublicClient({
chain: bsc,
transport: http(process.env.BSC_RPC_URL),
});
Read agent state
const state = await client.readContract({
address: BAP578_ADDRESS,
abi: BAP578_ABI,
functionName: "getAgentState",
args: [tokenId],
});
Read events
const events = await client.getContractEvents({
address: BAP578_ADDRESS,
abi: BAP578_ABI,
eventName: "AgentCreated",
fromBlock: deploymentBlock,
toBlock: "latest",
});
Vault Integrity Verification
The vault is the off-chain memory layer. Verification ensures it has not been tampered with.
Verification algorithm
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}` };
}
}
Verification states
- Verified — hash matches; off-chain data is authentic.
- Hash mismatch — content has been modified since registration.
- Zero hash — no vault data was registered; skip verification.
- Fetch error — URI unreachable; vault may be offline.
Event Indexing
Building a simple indexer
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;
}
Reconstructing agent history
async function getAgentHistory(tokenId) {
const allEvents = await indexAllEvents();
return allEvents.filter((e) => {
const args = e.args;
return args.tokenId && args.tokenId.toString() === tokenId.toString();
});
}
Real-time monitoring
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}`);
});
}
Common Scan Patterns
Check if a token exists
try {
await contract.ownerOf(tokenId);
// Token exists
} catch {
// Token does not exist or was burned
}
Find agents with logic contracts
const agents = await scanAllAgents();
const withLogic = agents.filter(
(a) => a.logicAddress !== ethers.ZeroAddress
);
Find inactive agents with balance
const atRisk = agents.filter(
(a) => !a.active && parseFloat(a.balance) > 0
);
Find recently created agents
const recentEvents = await contract.queryFilter(
contract.filters.AgentCreated(),
-1000 // last 1000 blocks
);
Output Format
When asked for scanning help, respond with:
- What to scan (single agent, portfolio, bulk, events)
- Complete code (setup + query + processing)
- Expected output shape (JSON structure)
- Verification steps (how to cross-check results)
- Performance notes (rate limits, batching for bulk scans)
Data Export Formats
JSON export (single agent)
{
"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..."}
]
}
CSV export (bulk scan)
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
Generating CSV from scan results
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");
}
Advanced Scan Patterns
Ownership concentration analysis
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) + "%"
};
}
Agent health scoring
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 };
}
Time-series analysis
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 }));
}
Funding flow analysis
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"
}));
}
Performance Considerations
- Rate limiting: Public RPC endpoints have request limits. Use batched calls or a dedicated RPC provider for bulk scanning.
- Block range limits: Some providers limit
getLogsblock ranges. Paginate event queries in chunks of 5000-10000 blocks. - Caching: Cache agent state reads for dashboard UIs. Invalidate on new events.
- Parallel reads: Use
Promise.allfor independent agent lookups, but respect rate limits. - Incremental indexing: Store the last indexed block number and resume from there on restart.
- Batch RPC calls: Use
ethers.JsonRpcProvider.send("eth_call", [...])with multicall for efficient bulk reads.
Related Skills
bap578bap578-analyticsfrontend-web3-bap578
微信扫一扫