
Identity
- 185 installs
- 9 repo stars
- Updated July 16, 2026
- aibtcdev/skills
Manage agent identities, wallet keys, credentials, and persona boundaries so aibtc agents authenticate safely across chains and APIs.
About
Handles aibtc agent identity and security: wallet binding, credential management, persona isolation, and auth policies so autonomous agents access chains and APIs with audited least-privilege controls before launch.
- Wallet identity binding
- Credential rotation
- Agent persona isolation
- Auth policy enforcement
- Least-privilege access
Identity by the numbers
- 185 all-time installs (skills.sh)
- Ranked #805 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aibtcdev/skills --skill identityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 185 |
|---|---|
| repo stars | ★ 9 |
| Last updated | July 16, 2026 |
| Repository | aibtcdev/skills ↗ |
What it does
Manage agent identities, wallet keys, credentials, and persona boundaries so aibtc agents authenticate safely across chains and APIs.
Files
Identity Skill
Provides ERC-8004 on-chain agent identity operations using the identity-registry contract. Read operations (get, get-metadata, get-last-id) work without a wallet. Write operations (register, set-uri, set-metadata, set-approval, set-wallet, unset-wallet, transfer) require an unlocked wallet.
Usage
bun run identity/identity.ts <subcommand> [options]Subcommands
register
Register a new agent identity on-chain using the ERC-8004 identity registry. Returns a transaction ID. Check the transaction result to get the assigned agent ID. Requires an unlocked wallet.
bun run identity/identity.ts register [--uri <uri>] [--metadata <json>] [--fee <fee>] [--sponsored]Options:
--uri(optional) — URI pointing to agent metadata (IPFS, HTTP, etc.)--metadata(optional) — JSON array of{"key": "...", "value": "<hex>"}pairs (values are hex-encoded buffers)--fee(optional) — Fee preset (low,medium,high) or micro-STX amount--sponsored(flag) — Submit as a sponsored transaction
Output:
{
"success": true,
"txid": "0xabc...",
"message": "Identity registration transaction submitted. Check transaction result to get your agent ID.",
"network": "mainnet",
"explorerUrl": "https://explorer.hiro.so/txid/0xabc..."
}get
Get agent identity information from the ERC-8004 identity registry. Returns owner address, URI, and wallet if set.
bun run identity/identity.ts get --agent-id <id>Options:
--agent-id(required) — Agent ID to look up (non-negative integer)
Output:
{
"success": true,
"agentId": 42,
"owner": "SP1...",
"uri": "ipfs://...",
"wallet": "SP2...",
"network": "mainnet"
}set-uri
Update the URI for an agent identity. Caller must be the agent owner or an approved operator. Requires an unlocked wallet.
bun run identity/identity.ts set-uri --agent-id <id> --uri <uri> [--fee <fee>] [--sponsored]Options:
--agent-id(required) — Agent ID to update (non-negative integer)--uri(required) — New URI pointing to agent metadata (IPFS, HTTP, etc.)--fee(optional) — Fee preset (low,medium,high) or micro-STX amount--sponsored(flag) — Submit as a sponsored transaction
Output:
{
"success": true,
"txid": "0xdef...",
"message": "Identity URI update transaction submitted.",
"agentId": 42,
"uri": "ipfs://newuri...",
"network": "mainnet",
"explorerUrl": "https://explorer.hiro.so/txid/0xdef..."
}set-metadata
Set a metadata key-value pair for an agent identity. Value must be a hex-encoded buffer (max 512 bytes). The key agentWallet is reserved and will be rejected by the contract. Caller must be the agent owner or an approved operator. Requires an unlocked wallet.
bun run identity/identity.ts set-metadata --agent-id <id> --key <key> --value <hex> [--fee <fee>] [--sponsored]Options:
--agent-id(required) — Agent ID to update (non-negative integer)--key(required) — Metadata key (string)--value(required) — Metadata value as a hex-encoded buffer (e.g.,616c696365for "alice")--fee(optional) — Fee preset (low,medium,high) or micro-STX amount--sponsored(flag) — Submit as a sponsored transaction
Output:
{
"success": true,
"txid": "0xghi...",
"message": "Metadata set transaction submitted.",
"agentId": 42,
"key": "name",
"valueHex": "616c696365",
"network": "mainnet",
"explorerUrl": "https://explorer.hiro.so/txid/0xghi..."
}set-approval
Approve or revoke an operator for an agent identity. Approved operators can update URI, metadata, and wallet on behalf of the owner. Only the NFT owner can call this. Requires an unlocked wallet.
bun run identity/identity.ts set-approval --agent-id <id> --operator <address> [--approved] [--fee <fee>] [--sponsored]Options:
--agent-id(required) — Agent ID to update (non-negative integer)--operator(required) — Stacks address of the operator to approve or revoke--approved(flag) — Grant approval (omit to revoke)--fee(optional) — Fee preset (low,medium,high) or micro-STX amount--sponsored(flag) — Submit as a sponsored transaction
Output:
{
"success": true,
"txid": "0xjkl...",
"message": "Operator SP3... approved for agent 42.",
"agentId": 42,
"operator": "SP3...",
"approved": true,
"network": "mainnet",
"explorerUrl": "https://explorer.hiro.so/txid/0xjkl..."
}set-wallet
Set the agent wallet for an identity to tx-sender (the active wallet address). This links the active Stacks address to the agent ID without requiring a separate signature. Caller must be the agent owner or an approved operator. Requires an unlocked wallet.
bun run identity/identity.ts set-wallet --agent-id <id> [--fee <fee>] [--sponsored]Options:
--agent-id(required) — Agent ID to update (non-negative integer)--fee(optional) — Fee preset (low,medium,high) or micro-STX amount--sponsored(flag) — Submit as a sponsored transaction
Output:
{
"success": true,
"txid": "0xmno...",
"message": "Agent wallet set to tx-sender (SP1...) for agent 42.",
"agentId": 42,
"wallet": "SP1...",
"network": "mainnet",
"explorerUrl": "https://explorer.hiro.so/txid/0xmno..."
}unset-wallet
Remove the agent wallet association from an agent identity. Caller must be the agent owner or an approved operator. Requires an unlocked wallet.
bun run identity/identity.ts unset-wallet --agent-id <id> [--fee <fee>] [--sponsored]Options:
--agent-id(required) — Agent ID to update (non-negative integer)--fee(optional) — Fee preset (low,medium,high) or micro-STX amount--sponsored(flag) — Submit as a sponsored transaction
Output:
{
"success": true,
"txid": "0xpqr...",
"message": "Agent wallet cleared for agent 42.",
"agentId": 42,
"network": "mainnet",
"explorerUrl": "https://explorer.hiro.so/txid/0xpqr..."
}transfer
Transfer an agent identity NFT to a new owner. The active wallet (tx-sender) must equal the current owner. Transfer automatically clears the agent wallet association. Requires an unlocked wallet.
bun run identity/identity.ts transfer --agent-id <id> --recipient <address> [--fee <fee>] [--sponsored]Options:
--agent-id(required) — Agent ID (token ID) to transfer (non-negative integer)--recipient(required) — Stacks address of the new owner--fee(optional) — Fee preset (low,medium,high) or micro-STX amount--sponsored(flag) — Submit as a sponsored transaction
Output:
{
"success": true,
"txid": "0xstu...",
"message": "Identity NFT transfer submitted for agent 42.",
"agentId": 42,
"sender": "SP1...",
"recipient": "SP4...",
"network": "mainnet",
"explorerUrl": "https://explorer.hiro.so/txid/0xstu..."
}get-metadata
Read a metadata value by key from the ERC-8004 identity registry. Returns the raw buffer value as a hex string. Does not require a wallet.
bun run identity/identity.ts get-metadata --agent-id <id> --key <key>Options:
--agent-id(required) — Agent ID to query (non-negative integer)--key(required) — Metadata key to read
Output:
{
"success": true,
"agentId": 42,
"key": "name",
"valueHex": "616c696365",
"network": "mainnet"
}get-last-id
Get the most recently minted agent ID from the ERC-8004 identity registry. Returns null if no agents have been registered. Does not require a wallet.
bun run identity/identity.ts get-last-idOutput:
{
"success": true,
"lastAgentId": 99,
"network": "mainnet"
}Notes
- Read operations (get, get-metadata, get-last-id) work without a wallet
- Write operations require an unlocked wallet (
bun run wallet/wallet.ts unlock) - Agent IDs are assigned by the contract upon registration — check the transaction result to find your assigned ID
- Operator approvals allow a delegate address to update URI, metadata, and wallet for an agent
- Transfer automatically clears the agent wallet association; use
set-walletafter transfer if needed - The
agentWalletkey is reserved — useset-wallet/unset-walletsubcommands instead
Identity Agent
This agent manages ERC-8004 on-chain agent identities using the identity-registry contract. It handles registration (minting a sequential agent ID), updating identity attributes (URI, metadata, approvals, wallet), NFT transfers, and read-only queries. Read operations (get, get-metadata, get-last-id) work without a wallet. Write operations require an unlocked wallet.
Prerequisites
- For write operations (register, set-uri, set-metadata, set-approval, set-wallet, unset-wallet, transfer): wallet must be unlocked — run
bun run wallet/wallet.ts unlockfirst - For read operations (get, get-metadata, get-last-id): no wallet required
- Network must be configured (defaults to testnet; set
NETWORK=mainnetfor mainnet) - The agent ID must already exist on-chain before running set-uri, set-metadata, set-approval, set-wallet, unset-wallet, or transfer
Decision Logic
| Goal | Subcommand |
|---|---|
| Register a new on-chain agent identity | register — returns txid; check tx result for assigned agent ID |
| Look up owner, URI, and wallet for an agent | get --agent-id <id> — read-only, no wallet needed |
| Update the URI pointing to agent metadata | set-uri --agent-id <id> --uri <uri> — caller must be owner or operator |
| Store a key-value metadata pair on the agent | set-metadata --agent-id <id> --key <k> --value <hex> — value is hex-encoded buffer |
| Read a metadata value back from the registry | get-metadata --agent-id <id> --key <k> — read-only |
| Grant or revoke an operator for the agent | set-approval --agent-id <id> --operator <addr> — only NFT owner can call |
| Link the active wallet address to the agent ID | set-wallet --agent-id <id> — sets tx-sender as the agent wallet |
| Remove the wallet link from an agent identity | unset-wallet --agent-id <id> — caller must be owner or operator |
| Transfer the identity NFT to a new owner | transfer --agent-id <id> --recipient <addr> — clears wallet automatically |
| Find the most recently minted agent ID | get-last-id — read-only |
Safety Checks
set-approvalcan only be called by the NFT owner — not by delegated operators; verify ownership withgetfirsttransferis irreversible — confirm the recipient address is correct; it also clears the agent wallet link- Metadata values must be hex-encoded buffers (max 512 bytes); the key
agentWalletis reserved by the contract — useset-wallet/unset-walletinstead - Write operations submit Stacks L2 transactions — check status with
bun run stx/stx.ts get-transaction-status --txid <txid>after submission - Never include plaintext data directly in
--value; always hex-encode first (e.g.,echo -n "alice" | xxd -pgives616c696365)
Error Handling
| Error message | Cause | Fix |
|---|---|---|
| "No active wallet. Please unlock your wallet first." | Write command called without an unlocked wallet | Run bun run wallet/wallet.ts unlock --password <password> |
| "--agent-id must be a non-negative integer" | Invalid or missing --agent-id value | Pass a non-negative integer (e.g., --agent-id 42) |
| "--value must be a non-empty, even-length hex string" | Metadata value is not valid hex | Hex-encode the value before passing (e.g., use xxd -p) |
| "metadata value for key ... exceeds 512 bytes" | Hex-encoded metadata buffer is too large | Reduce the value size to 512 bytes or less |
| "Agent ID not found" | get returned no identity for the given ID | The agent may not be registered yet; check get-last-id |
| "Metadata key not found for this agent" | get-metadata found no value for the key | The key was never set; use set-metadata to store it |
Output Handling
register: extracttxidand check transaction result to get the assignedagentId;explorerUrllinks directly to the transactionget: extractownerto verify ownership before write operations;walletmay be "(no wallet set)" if not linkedget-metadata: extractvalueHexand decode from hex to get the original valueget-last-id: extractlastAgentIdto discover the range of registered agent IDs- Write operations return
txidandexplorerUrl; the transaction may take 10–30 seconds to confirm on-chain success: falseresponses indicate a not-found condition, not an error — check themessagefield for details
Example Invocations
# Register a new on-chain agent identity with a metadata URI
bun run identity/identity.ts register --uri https://myagent.example.com/metadata.json
# Look up an agent's identity by agent ID
bun run identity/identity.ts get --agent-id 42
# Link the active wallet address to the agent
bun run identity/identity.ts set-wallet --agent-id 42#!/usr/bin/env bun
/**
* Identity skill CLI
* ERC-8004 on-chain agent identity management
*
* Usage: bun run identity/identity.ts <subcommand> [options]
*/
import { Command } from "commander";
import { NETWORK, getExplorerTxUrl } from "../src/lib/config/networks.js";
import { getWalletManager } from "../src/lib/services/wallet-manager.js";
import { Erc8004Service } from "../src/lib/services/erc8004.service.js";
import { resolveFee } from "../src/lib/utils/fee.js";
import { printJson, handleError } from "../src/lib/utils/cli.js";
// ---------------------------------------------------------------------------
// Identity helpers
// ---------------------------------------------------------------------------
/** Default read-only caller address per network (boot addresses) */
const DEFAULT_CALLER: Record<string, string> = {
mainnet: "SP000000000000000000002Q6VF78",
testnet: "ST000000000000000000002AMW42H",
};
/**
* Get the caller address for read-only calls.
* Prefers the active wallet address if available.
*/
function getCallerAddress(): string {
const walletManager = getWalletManager();
const sessionInfo = walletManager.getSessionInfo();
return sessionInfo?.address || DEFAULT_CALLER[NETWORK] || DEFAULT_CALLER.testnet;
}
/**
* Strip optional 0x prefix and validate a hex string.
* Optionally enforce exact byte count.
*/
function normalizeHex(hex: string, label: string, exactBytes?: number): string {
let normalized = hex;
if (normalized.startsWith("0x") || normalized.startsWith("0X")) {
normalized = normalized.slice(2);
}
if (
normalized.length === 0 ||
normalized.length % 2 !== 0 ||
!/^[0-9a-fA-F]+$/.test(normalized)
) {
throw new Error(`${label} must be a non-empty, even-length hex string`);
}
if (exactBytes !== undefined && normalized.length !== exactBytes * 2) {
throw new Error(
`${label} must be exactly ${exactBytes} bytes (${exactBytes * 2} hex characters)`
);
}
return normalized;
}
// ---------------------------------------------------------------------------
// Program
// ---------------------------------------------------------------------------
const program = new Command();
program
.name("identity")
.description(
"ERC-8004 on-chain agent identity: register identities, update URI and metadata, " +
"manage operator approvals, set/unset wallet, transfer identity NFTs, and query identity info"
)
.version("0.1.0");
// ---------------------------------------------------------------------------
// register
// ---------------------------------------------------------------------------
program
.command("register")
.description(
"Register a new agent identity on-chain using ERC-8004 identity registry. " +
"Returns a transaction ID. Check the transaction result to get the assigned agent ID. " +
"Requires an unlocked wallet."
)
.option(
"--uri <uri>",
"URI pointing to agent metadata (IPFS, HTTP, etc.)"
)
.option(
"--metadata <json>",
'JSON array of {key, value} pairs where value is a hex-encoded buffer (e.g., \'[{"key":"name","value":"616c696365"}]\')'
)
.option(
"--fee <fee>",
'Fee preset ("low", "medium", "high") or micro-STX amount'
)
.option(
"--sponsored",
"Submit as a sponsored transaction",
false
)
.action(
async (opts: {
uri?: string;
metadata?: string;
fee?: string;
sponsored: boolean;
}) => {
try {
const walletManager = getWalletManager();
const account = walletManager.getActiveAccount();
if (!account) {
throw new Error("No active wallet. Please unlock your wallet first.");
}
const service = new Erc8004Service(NETWORK);
// Parse metadata if provided
let parsedMetadata: Array<{ key: string; value: Buffer }> | undefined;
if (opts.metadata) {
let rawMetadata: unknown;
try {
rawMetadata = JSON.parse(opts.metadata);
} catch {
throw new Error("--metadata must be valid JSON");
}
if (!Array.isArray(rawMetadata)) {
throw new Error("--metadata must be a JSON array");
}
parsedMetadata = rawMetadata.map((m: unknown) => {
if (
typeof m !== "object" ||
m === null ||
typeof (m as Record<string, unknown>).key !== "string" ||
typeof (m as Record<string, unknown>).value !== "string"
) {
throw new Error('Each metadata entry must have string "key" and "value" fields');
}
const entry = m as { key: string; value: string };
const normalized = normalizeHex(
entry.value,
`metadata value for key "${entry.key}"`
);
const buf = Buffer.from(normalized, "hex");
if (buf.length > 512) {
throw new Error(
`metadata value for key "${entry.key}" exceeds 512 bytes (got ${buf.length})`
);
}
return { key: entry.key, value: buf };
});
}
const feeAmount = opts.fee
? await resolveFee(opts.fee, NETWORK, "contract_call")
: undefined;
const result = await service.registerIdentity(
account,
opts.uri,
parsedMetadata,
feeAmount,
opts.sponsored
);
printJson({
success: true,
txid: result.txid,
message:
"Identity registration transaction submitted. " +
"Check transaction result to get your agent ID.",
network: NETWORK,
explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
});
} catch (error) {
handleError(error);
}
}
);
// ---------------------------------------------------------------------------
// get
// ---------------------------------------------------------------------------
program
.command("get")
.description(
"Get agent identity information from the ERC-8004 identity registry. " +
"Returns owner address, URI, and wallet address if set."
)
.requiredOption(
"--agent-id <id>",
"Agent ID to look up (non-negative integer)"
)
.action(async (opts: { agentId: string }) => {
try {
const agentId = parseInt(opts.agentId, 10);
if (isNaN(agentId) || agentId < 0) {
throw new Error("--agent-id must be a non-negative integer");
}
const service = new Erc8004Service(NETWORK);
const callerAddress = getCallerAddress();
const identity = await service.getIdentity(agentId, callerAddress);
if (!identity) {
printJson({
success: false,
agentId,
message: "Agent ID not found",
});
return;
}
printJson({
success: true,
agentId: identity.agentId,
owner: identity.owner,
uri: identity.uri || "(no URI set)",
wallet: identity.wallet || "(no wallet set)",
network: NETWORK,
});
} catch (error) {
handleError(error);
}
});
// ---------------------------------------------------------------------------
// set-uri
// ---------------------------------------------------------------------------
program
.command("set-uri")
.description(
"Update the URI for an agent identity in the ERC-8004 identity registry. " +
"Caller must be the agent owner or an approved operator. Requires an unlocked wallet."
)
.requiredOption(
"--agent-id <id>",
"Agent ID to update (non-negative integer)"
)
.requiredOption(
"--uri <uri>",
"New URI pointing to agent metadata (IPFS, HTTP, etc.)"
)
.option(
"--fee <fee>",
'Fee preset ("low", "medium", "high") or micro-STX amount'
)
.option(
"--sponsored",
"Submit as a sponsored transaction",
false
)
.action(
async (opts: {
agentId: string;
uri: string;
fee?: string;
sponsored: boolean;
}) => {
try {
const walletManager = getWalletManager();
const account = walletManager.getActiveAccount();
if (!account) {
throw new Error("No active wallet. Please unlock your wallet first.");
}
const agentId = parseInt(opts.agentId, 10);
if (isNaN(agentId) || agentId < 0) {
throw new Error("--agent-id must be a non-negative integer");
}
const service = new Erc8004Service(NETWORK);
const feeAmount = opts.fee
? await resolveFee(opts.fee, NETWORK, "contract_call")
: undefined;
const result = await service.updateIdentityUri(
account,
agentId,
opts.uri,
feeAmount,
opts.sponsored
);
printJson({
success: true,
txid: result.txid,
message: "Identity URI update transaction submitted.",
agentId,
uri: opts.uri,
network: NETWORK,
explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
});
} catch (error) {
handleError(error);
}
}
);
// ---------------------------------------------------------------------------
// set-metadata
// ---------------------------------------------------------------------------
program
.command("set-metadata")
.description(
"Set a metadata key-value pair for an agent identity in the ERC-8004 identity registry. " +
"Value must be a hex-encoded buffer (max 512 bytes). " +
'The key "agentWallet" is reserved and will be rejected by the contract. ' +
"Caller must be the agent owner or an approved operator. Requires an unlocked wallet."
)
.requiredOption(
"--agent-id <id>",
"Agent ID to update (non-negative integer)"
)
.requiredOption(
"--key <key>",
"Metadata key (string)"
)
.requiredOption(
"--value <hex>",
"Metadata value as a hex-encoded buffer (e.g., 616c696365 for 'alice')"
)
.option(
"--fee <fee>",
'Fee preset ("low", "medium", "high") or micro-STX amount'
)
.option(
"--sponsored",
"Submit as a sponsored transaction",
false
)
.action(
async (opts: {
agentId: string;
key: string;
value: string;
fee?: string;
sponsored: boolean;
}) => {
try {
const walletManager = getWalletManager();
const account = walletManager.getActiveAccount();
if (!account) {
throw new Error("No active wallet. Please unlock your wallet first.");
}
const agentId = parseInt(opts.agentId, 10);
if (isNaN(agentId) || agentId < 0) {
throw new Error("--agent-id must be a non-negative integer");
}
const normalized = normalizeHex(opts.value, "--value");
const buf = Buffer.from(normalized, "hex");
if (buf.length > 512) {
throw new Error(`--value exceeds 512 bytes (got ${buf.length})`);
}
const service = new Erc8004Service(NETWORK);
const feeAmount = opts.fee
? await resolveFee(opts.fee, NETWORK, "contract_call")
: undefined;
const result = await service.setMetadata(
account,
agentId,
opts.key,
buf,
feeAmount,
opts.sponsored
);
printJson({
success: true,
txid: result.txid,
message: "Metadata set transaction submitted.",
agentId,
key: opts.key,
valueHex: normalized,
network: NETWORK,
explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
});
} catch (error) {
handleError(error);
}
}
);
// ---------------------------------------------------------------------------
// set-approval
// ---------------------------------------------------------------------------
program
.command("set-approval")
.description(
"Approve or revoke an operator for an agent identity in the ERC-8004 identity registry. " +
"Approved operators can update URI, metadata, and wallet on behalf of the owner. " +
"Only the NFT owner can call this. Requires an unlocked wallet."
)
.requiredOption(
"--agent-id <id>",
"Agent ID to update (non-negative integer)"
)
.requiredOption(
"--operator <address>",
"Stacks address of the operator to approve or revoke"
)
.option(
"--approved",
"Grant approval (omit to revoke)",
false
)
.option(
"--fee <fee>",
'Fee preset ("low", "medium", "high") or micro-STX amount'
)
.option(
"--sponsored",
"Submit as a sponsored transaction",
false
)
.action(
async (opts: {
agentId: string;
operator: string;
approved: boolean;
fee?: string;
sponsored: boolean;
}) => {
try {
const walletManager = getWalletManager();
const account = walletManager.getActiveAccount();
if (!account) {
throw new Error("No active wallet. Please unlock your wallet first.");
}
const agentId = parseInt(opts.agentId, 10);
if (isNaN(agentId) || agentId < 0) {
throw new Error("--agent-id must be a non-negative integer");
}
const service = new Erc8004Service(NETWORK);
const feeAmount = opts.fee
? await resolveFee(opts.fee, NETWORK, "contract_call")
: undefined;
const result = await service.setApprovalForAll(
account,
agentId,
opts.operator,
opts.approved,
feeAmount,
opts.sponsored
);
printJson({
success: true,
txid: result.txid,
message: opts.approved
? `Operator ${opts.operator} approved for agent ${agentId}.`
: `Operator ${opts.operator} revoked for agent ${agentId}.`,
agentId,
operator: opts.operator,
approved: opts.approved,
network: NETWORK,
explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
});
} catch (error) {
handleError(error);
}
}
);
// ---------------------------------------------------------------------------
// set-wallet
// ---------------------------------------------------------------------------
program
.command("set-wallet")
.description(
"Set the agent wallet for an identity to tx-sender (the active wallet address). " +
"This links the Stacks address to the agent ID without requiring a signature. " +
"Caller must be the agent owner or an approved operator. Requires an unlocked wallet."
)
.requiredOption(
"--agent-id <id>",
"Agent ID to update (non-negative integer)"
)
.option(
"--fee <fee>",
'Fee preset ("low", "medium", "high") or micro-STX amount'
)
.option(
"--sponsored",
"Submit as a sponsored transaction",
false
)
.action(
async (opts: {
agentId: string;
fee?: string;
sponsored: boolean;
}) => {
try {
const walletManager = getWalletManager();
const account = walletManager.getActiveAccount();
if (!account) {
throw new Error("No active wallet. Please unlock your wallet first.");
}
const agentId = parseInt(opts.agentId, 10);
if (isNaN(agentId) || agentId < 0) {
throw new Error("--agent-id must be a non-negative integer");
}
const service = new Erc8004Service(NETWORK);
const feeAmount = opts.fee
? await resolveFee(opts.fee, NETWORK, "contract_call")
: undefined;
const result = await service.setAgentWalletDirect(
account,
agentId,
feeAmount,
opts.sponsored
);
printJson({
success: true,
txid: result.txid,
message: `Agent wallet set to tx-sender (${account.address}) for agent ${agentId}.`,
agentId,
wallet: account.address,
network: NETWORK,
explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
});
} catch (error) {
handleError(error);
}
}
);
// ---------------------------------------------------------------------------
// unset-wallet
// ---------------------------------------------------------------------------
program
.command("unset-wallet")
.description(
"Remove the agent wallet association from an agent identity in the ERC-8004 identity registry. " +
"Caller must be the agent owner or an approved operator. Requires an unlocked wallet."
)
.requiredOption(
"--agent-id <id>",
"Agent ID to update (non-negative integer)"
)
.option(
"--fee <fee>",
'Fee preset ("low", "medium", "high") or micro-STX amount'
)
.option(
"--sponsored",
"Submit as a sponsored transaction",
false
)
.action(
async (opts: {
agentId: string;
fee?: string;
sponsored: boolean;
}) => {
try {
const walletManager = getWalletManager();
const account = walletManager.getActiveAccount();
if (!account) {
throw new Error("No active wallet. Please unlock your wallet first.");
}
const agentId = parseInt(opts.agentId, 10);
if (isNaN(agentId) || agentId < 0) {
throw new Error("--agent-id must be a non-negative integer");
}
const service = new Erc8004Service(NETWORK);
const feeAmount = opts.fee
? await resolveFee(opts.fee, NETWORK, "contract_call")
: undefined;
const result = await service.unsetAgentWallet(
account,
agentId,
feeAmount,
opts.sponsored
);
printJson({
success: true,
txid: result.txid,
message: `Agent wallet cleared for agent ${agentId}.`,
agentId,
network: NETWORK,
explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
});
} catch (error) {
handleError(error);
}
}
);
// ---------------------------------------------------------------------------
// transfer
// ---------------------------------------------------------------------------
program
.command("transfer")
.description(
"Transfer an agent identity NFT to a new owner. " +
"The active wallet (tx-sender) must equal the current owner. " +
"Transfer automatically clears the agent wallet association. Requires an unlocked wallet."
)
.requiredOption(
"--agent-id <id>",
"Agent ID (token ID) to transfer (non-negative integer)"
)
.requiredOption(
"--recipient <address>",
"Stacks address of the new owner"
)
.option(
"--fee <fee>",
'Fee preset ("low", "medium", "high") or micro-STX amount'
)
.option(
"--sponsored",
"Submit as a sponsored transaction",
false
)
.action(
async (opts: {
agentId: string;
recipient: string;
fee?: string;
sponsored: boolean;
}) => {
try {
const walletManager = getWalletManager();
const account = walletManager.getActiveAccount();
if (!account) {
throw new Error("No active wallet. Please unlock your wallet first.");
}
const agentId = parseInt(opts.agentId, 10);
if (isNaN(agentId) || agentId < 0) {
throw new Error("--agent-id must be a non-negative integer");
}
const service = new Erc8004Service(NETWORK);
const feeAmount = opts.fee
? await resolveFee(opts.fee, NETWORK, "contract_call")
: undefined;
const result = await service.transferIdentity(
account,
agentId,
account.address,
opts.recipient,
feeAmount,
opts.sponsored
);
printJson({
success: true,
txid: result.txid,
message: `Identity NFT transfer submitted for agent ${agentId}.`,
agentId,
sender: account.address,
recipient: opts.recipient,
network: NETWORK,
explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
});
} catch (error) {
handleError(error);
}
}
);
// ---------------------------------------------------------------------------
// get-metadata
// ---------------------------------------------------------------------------
program
.command("get-metadata")
.description(
"Read a metadata value by key from the ERC-8004 identity registry. " +
"Returns the raw buffer value as a hex string. Does not require a wallet."
)
.requiredOption(
"--agent-id <id>",
"Agent ID to query (non-negative integer)"
)
.requiredOption(
"--key <key>",
"Metadata key to read"
)
.action(async (opts: { agentId: string; key: string }) => {
try {
const agentId = parseInt(opts.agentId, 10);
if (isNaN(agentId) || agentId < 0) {
throw new Error("--agent-id must be a non-negative integer");
}
const service = new Erc8004Service(NETWORK);
const callerAddress = getCallerAddress();
const value = await service.getMetadata(agentId, opts.key, callerAddress);
if (value === null) {
printJson({
success: false,
agentId,
key: opts.key,
message: "Metadata key not found for this agent",
network: NETWORK,
});
return;
}
printJson({
success: true,
agentId,
key: opts.key,
valueHex: value,
network: NETWORK,
});
} catch (error) {
handleError(error);
}
});
// ---------------------------------------------------------------------------
// get-last-id
// ---------------------------------------------------------------------------
program
.command("get-last-id")
.description(
"Get the most recently minted agent ID from the ERC-8004 identity registry. " +
"Returns null if no agents have been registered. Does not require a wallet."
)
.action(async () => {
try {
const service = new Erc8004Service(NETWORK);
const callerAddress = getCallerAddress();
const lastId = await service.getLastTokenId(callerAddress);
if (lastId === null) {
printJson({
success: false,
message: "No agents have been registered yet",
network: NETWORK,
});
return;
}
printJson({
success: true,
lastAgentId: lastId,
network: NETWORK,
});
} catch (error) {
handleError(error);
}
});
// ---------------------------------------------------------------------------
// Parse
// ---------------------------------------------------------------------------
program.parse(process.argv);