SDK · Identity
ERC-8004: agent identity
An ERC-8004 identity is what makes an agent discoverable: a token in the
identity registry whose tokenURI is the agent's registration record — its
name, what it does, and the endpoint to reach it at. Buyers browsing the BNB
agent economy find sellers this way, and the ERC-8183 job escrow
(Hire BNB agents) is what they do next.
The registry is a plain ERC-721 at the address ERC8183_ADDRESSES[chainId].registry
already carries — 0x8004A169… on BSC mainnet (56), 0x8004A818… on testnet
(97). register and setAgentURI are nonpayable: no protocol fee, only gas.
Registration is two-phase, because the record embeds the very id the mint
assigns. Mint with an empty registrations list, then patch the returned
agentId in and write the record back.
Grant the capability
An agent registering its own identity is exactly the case session keys exist for. Grant it the two selectors it needs and nothing else:
import { createClient, erc8004RegisterPermissions, BNB, signerFromPrivateKey } from "@altananetwork/sdk";
const client = createClient({ chains: [BNB] });
const admin = signerFromPrivateKey("0x...");
const wallet = await client.createWallet({ signer: admin });
const session = await client.grantSession({
wallet,
signer: admin,
permissions: {
calls: erc8004RegisterPermissions(56),
// A small native cap covers gas; nothing else can leave the wallet.
spend: [{ limit: 20_000_000_000_000_000n, period: "day" }], // 0.02 BNB
},
expiry: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
});erc8004RegisterPermissions(chainId) returns two { to, signature } rules —
AND semantics, so the session may call that selector at that address only:
[
{ to: "0x8004A169…", signature: "register(string,(string,bytes)[])" },
{ to: "0x8004A169…", signature: "setAgentURI(uint256,string)" },
]Note also that the scope is per-selector, not per-agent: a session holding these permissions can rewrite the record of any agent the wallet owns.
Register an agent
import {
registerErc8004Agent,
setErc8004AgentUri,
encodeErc8004AgentUri,
withErc8004Registration,
BNB,
} from "@altananetwork/sdk";
// Phase 1: registrations is empty — the id does not exist yet.
const record = {
type: "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
name: "Vault Sentinel",
description: "Watches Venus positions and alerts on liquidation risk.",
image: "",
services: [{ name: "A2A", endpoint: "https://sentinel.example/.well-known/agent-card.json" }],
registrations: [],
} as const;
const { agentId } = await registerErc8004Agent(
session,
{ agentUri: encodeErc8004AgentUri(record) },
{ network: BNB },
);
// Phase 2: bind the assigned id to this registry and publish the finished record.
await setErc8004AgentUri(
session,
{ agentId, agentUri: encodeErc8004AgentUri(withErc8004Registration(record, agentId, 56)) },
{ network: BNB },
);withErc8004Registration is pure — it returns a copy whose registrations
names this chain ({ agentId, agentRegistry: "eip155:56:0x8004A169…" }) and
leaves entries for other chains alone.
The admin path is registerErc8004Agent(wallet, signer, params, opts), same as
every other write in the SDK.
Read an identity back
import { getErc8004Agent, decodeErc8004AgentUri, BNB } from "@altananetwork/sdk";
const { owner, agentUri } = await getErc8004Agent(BNB, agentId);
const record = decodeErc8004AgentUri(agentUri); // throws on a non-data URIThere is no reverse lookup on the registry — you need the agentId, which is
why registration returns it. Persist it.
Records are byte-exact
encodeErc8004AgentUri produces data:application/json;base64,<canonical JSON>
— keys sorted at every depth, no incidental whitespace, non-ASCII escaped as
\uXXXX. This is byte-identical to what @bnbagent's TypeScript and Python
SDKs produce for the same record, so a record written here hashes the same as
one written there. Do not re-serialize the decoded object with a plain
JSON.stringify and expect the hashes to match.
The same care applies to the session itself. Porto derives the session key's
hash from its exact permissions and expiry, so a session that crosses a process
boundary must survive the round trip unchanged — BNB Agent Studio serializes
sessions between processes, and a JSON round-trip that re-cases an address
changes the key hash and makes the next call fail with an unknown key. Persist
and restore the granted values verbatim (see packages/mcp/src/sessions.ts for
the pattern this repo uses).
Through the MCP server
The same flow is three tools — erc8004_register (which drives both phases and
returns the agentId even if the second one fails), erc8004_set_agent_uri,
and erc8004_show. See MCP Tools.