Skip to content

Step 5: Spawn Child Agents

Goal

Register a child agent under an existing parent agent. The child inherits the parent's Haftungsperson (legally responsible human) and is tracked in the on-chain lineage tree. This is for agents that programmatically create sub-agents for delegation, specialization, or scaling.

Lineage Rules

Before spawning a child, understand these on-chain constraints:

Rule Constraint
Parent must be Active A Suspended, Revoked, or Terminated parent cannot spawn children
Maximum generation depth 10 (configurable by the registry owner). Root agents are generation 0; their children are generation 1, etc.
Haftungsperson inheritance Children automatically inherit the parent's Haftungsperson. You cannot override this.
Unique wallet Each child agent needs its own unique Ethereum wallet address
Lineage tracking Parent-child relationships are stored on-chain via getChildren(parentId)

Prerequisites

  1. A registered, Active parent agent (from Step 2)
  2. A new private key for the child agent (from Step 1)
from agentenregister import AgentRegistry

registry = AgentRegistry(
    chain="base_sepolia",
    registry_address="0x2EFaB5B3BEf49E56a6Ce1dcB1A39EF63C312EA23",
    private_key="0x_parent_agent_private_key",
    relayer_url="https://relay.theagentregistry.org",
)

# Generate a key for the child first (see Step 1)
CHILD_WALLET = "0x_child_agent_wallet_address"

child_id = registry.register_child(
    parent_agent_id=42,
    child_wallet=CHILD_WALLET,
    capabilities=["data_analysis", "report_generation"],
    operational_scope="Data analysis sub-agent for report generation",
)
print(f"Child agent ID: {child_id}")
import { AgentRegistry } from "@agentenregister/sdk";

const registry = new AgentRegistry({
    chain: "base_sepolia",
    registryAddress: "0x2EFaB5B3BEf49E56a6Ce1dcB1A39EF63C312EA23",
    privateKey: "0x_parent_agent_private_key",
    relayerUrl: "https://relay.theagentregistry.org",
});

// Generate a key for the child first (see Step 1)
const CHILD_WALLET = "0x_child_agent_wallet_address";

const childId = await registry.registerChild({
    parentAgentId: 42,
    childWallet: CHILD_WALLET,
    capabilities: ["data_analysis", "report_generation"],
    operationalScope: "Data analysis sub-agent for report generation",
});
console.log(`Child agent ID: ${childId}`);

Method B: Raw Implementation (No SDK)

Spawning a child is identical to registration (Step 2, Method B) with two differences:

  1. Set parentAgentId to the parent's agent ID (instead of 0)
  2. The Haftungsperson must match the parent's Haftungsperson
from web3 import Web3
from eth_account import Account
from eth_account.messages import encode_structured_data
import requests
import json

# ── Configuration ─────────────────────────────────────────────
PRIVATE_KEY = "0x_parent_agent_private_key"
RELAYER_URL = "https://relay.theagentregistry.org"
REGISTRY_ADDRESS = "0x2EFaB5B3BEf49E56a6Ce1dcB1A39EF63C312EA23"
CHAIN_ID = 84532

PARENT_AGENT_ID = 42
CHILD_WALLET = "0x_child_agent_wallet_address"

account = Account.from_key(PRIVATE_KEY)

# ── First, look up the parent's Haftungsperson ────────────────
# (The SDK does this automatically via register_child)
# Query via REST API or direct contract call:
parent_resp = requests.get(
    f"https://api.theagentregistry.org/api/v1/agent/{PARENT_AGENT_ID}"
).json()
HAFTUNGSPERSON = parent_resp["agent"]["haftungsperson"]

# ── Get forwarder address and nonce ───────────────────────────
domain = requests.get(f"{RELAYER_URL}/domain").json()
FORWARDER_ADDRESS = domain["verifyingContract"]

nonce = int(
    requests.get(f"{RELAYER_URL}/nonce/{account.address}").json()["nonce"]
)

# ── Encode registerAgent calldata (with parent ID) ────────────
w3 = Web3()
ABI = json.loads(
    '[{"inputs":[{"name":"_haftungsperson","type":"address"},'
    '{"name":"_agentWallet","type":"address"},'
    '{"name":"_constitutionHash","type":"bytes32"},'
    '{"name":"_capabilityHash","type":"bytes32"},'
    '{"name":"_operationalScope","type":"string"},'
    '{"name":"_parentAgentId","type":"uint256"},'
    '{"name":"_selfModifying","type":"bool"}],'
    '"name":"registerAgent",'
    '"outputs":[{"name":"agentId","type":"uint256"}],'
    '"stateMutability":"nonpayable","type":"function"}]'
)
contract = w3.eth.contract(address=REGISTRY_ADDRESS, abi=ABI)

capabilities = ["data_analysis", "report_generation"]
cap_hash = Web3.keccak(
    text=json.dumps(sorted(capabilities), separators=(",", ":"))
)

calldata = contract.functions.registerAgent(
    Web3.to_checksum_address(HAFTUNGSPERSON),
    Web3.to_checksum_address(CHILD_WALLET),
    b'\x00' * 32,             # constitution hash (empty)
    cap_hash,
    "Data analysis sub-agent for report generation",
    PARENT_AGENT_ID,          # <-- parent agent ID (non-zero)
    False,                    # selfModifying
).build_transaction({"gas": 0, "gasPrice": 0, "nonce": 0})["data"]

# ── Sign EIP-712 typed data ───────────────────────────────────
typed_data = {
    "types": {
        "EIP712Domain": [
            {"name": "name", "type": "string"},
            {"name": "version", "type": "string"},
            {"name": "chainId", "type": "uint256"},
            {"name": "verifyingContract", "type": "address"},
        ],
        "ForwardRequest": [
            {"name": "from", "type": "address"},
            {"name": "to", "type": "address"},
            {"name": "value", "type": "uint256"},
            {"name": "gas", "type": "uint256"},
            {"name": "nonce", "type": "uint256"},
            {"name": "deadline", "type": "uint48"},
            {"name": "data", "type": "bytes"},
        ],
    },
    "primaryType": "ForwardRequest",
    "domain": {
        "name": "MinimalForwarder",
        "version": "1",
        "chainId": CHAIN_ID,
        "verifyingContract": FORWARDER_ADDRESS,
    },
    "message": {
        "from": account.address,
        "to": REGISTRY_ADDRESS,
        "value": 0,
        "gas": 800000,
        "nonce": nonce,
        "deadline": 0,
        "data": calldata,
    },
}

encoded = encode_structured_data(typed_data)
signed = account.sign_message(encoded)

# ── Submit to relayer ─────────────────────────────────────────
result = requests.post(f"{RELAYER_URL}/relay", json={
    "request": {
        "from": account.address,
        "to": REGISTRY_ADDRESS,
        "value": "0",
        "gas": "800000",
        "nonce": str(nonce),
        "deadline": "0",
        "data": calldata,
    },
    "signature": signed.signature.hex(),
}).json()

print(f"Child registered! TX: {result['transactionHash']}")
import { ethers } from "ethers";

// ── Configuration ─────────────────────────────────────────────
const PRIVATE_KEY = "0x_parent_agent_private_key";
const RELAYER_URL = "https://relay.theagentregistry.org";
const REGISTRY_ADDRESS = "0x2EFaB5B3BEf49E56a6Ce1dcB1A39EF63C312EA23";
const CHAIN_ID = 84532;

const PARENT_AGENT_ID = 42;
const CHILD_WALLET = "0x_child_agent_wallet_address";

const provider = new ethers.JsonRpcProvider("https://sepolia.base.org");
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);

// ── Look up parent's Haftungsperson ──────────────────────────
const parentRes = await fetch(
    `https://api.theagentregistry.org/api/v1/agent/${PARENT_AGENT_ID}`
);
const parentData = await parentRes.json();
const HAFTUNGSPERSON = parentData.agent.haftungsperson;

// ── Get forwarder address and nonce ───────────────────────────
const domainRes = await fetch(`${RELAYER_URL}/domain`);
const domain = await domainRes.json();
const FORWARDER_ADDRESS = domain.verifyingContract;

const nonceRes = await fetch(`${RELAYER_URL}/nonce/${wallet.address}`);
const nonce = (await nonceRes.json()).nonce;

// ── Encode registerAgent calldata (with parent ID) ────────────
const ABI = [
    "function registerAgent(address,address,bytes32,bytes32,string,uint256,bool) returns (uint256)",
];
const contract = new ethers.Contract(REGISTRY_ADDRESS, ABI, wallet);

const capabilities = ["data_analysis", "report_generation"];
const capHash = ethers.keccak256(
    ethers.toUtf8Bytes(JSON.stringify([...capabilities].sort()))
);

const calldata = contract.interface.encodeFunctionData("registerAgent", [
    ethers.getAddress(HAFTUNGSPERSON),
    ethers.getAddress(CHILD_WALLET),
    ethers.ZeroHash,           // constitution hash (empty)
    capHash,
    "Data analysis sub-agent for report generation",
    PARENT_AGENT_ID,           // <-- parent agent ID (non-zero)
    false,                     // selfModifying
]);

// ── Sign and submit (same as Step 2) ─────────────────────────
const request = {
    from: wallet.address,
    to: REGISTRY_ADDRESS,
    value: "0",
    gas: "800000",
    nonce: String(nonce),
    deadline: "0",
    data: calldata,
};

const signature = await wallet.signTypedData(
    {
        name: "MinimalForwarder",
        version: "1",
        chainId: BigInt(CHAIN_ID),
        verifyingContract: FORWARDER_ADDRESS,
    },
    {
        ForwardRequest: [
            { name: "from", type: "address" },
            { name: "to", type: "address" },
            { name: "value", type: "uint256" },
            { name: "gas", type: "uint256" },
            { name: "nonce", type: "uint256" },
            { name: "deadline", type: "uint48" },
            { name: "data", type: "bytes" },
        ],
    },
    request,
);

const relayRes = await fetch(`${RELAYER_URL}/relay`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ request, signature }),
});

const result = await relayRes.json();
console.log(`Child registered! TX: ${result.transactionHash}`);

Verify Lineage

After spawning, confirm the parent-child relationship on-chain:

children = registry.get_children(42)
print(f"Parent agent 42 has children: {children}")

child_info = registry.get_agent(child_id)
print(f"Child #{child_info.agent_id}:")
print(f"  Parent:       {child_info.parent_agent_id}")
print(f"  Generation:   {child_info.generation}")
print(f"  Haftungsp.:   {child_info.haftungsperson}")
const children = await registry.getChildren(42);
console.log(`Parent agent 42 has children: ${children}`);

const childInfo = await registry.getAgent(childId);
console.log(`Child #${childInfo.agentId}:`);
console.log(`  Parent:       ${childInfo.parentAgentId}`);
console.log(`  Generation:   ${childInfo.generation}`);
console.log(`  Haftungsperson: ${childInfo.haftungsperson}`);

Or query the REST API:

curl https://api.theagentregistry.org/api/v1/agent/42/lineage

Lineage Tree Example

Agent #1 (Generation 0, Root)
  |-- Agent #5 (Generation 1)
  |     |-- Agent #12 (Generation 2)
  |     |-- Agent #13 (Generation 2)
  |-- Agent #6 (Generation 1)
        |-- Agent #14 (Generation 2)
              |-- Agent #20 (Generation 3)

All agents in this tree share the same Haftungsperson (the one set when Agent #1 was registered).

Child Agent Responsibilities

Each child agent must independently:

  1. Attest compliance every 6 days (Step 3)
  2. Report revenue when applicable (Step 4)
  3. Use its own private key for signing (not the parent's key)

The child agent can use either the parent's signing wallet or its own wallet to attest and report, as long as the signer is the child's wallet address, creator, or Haftungsperson.

Common Errors

Error Cause Resolution
"Parent agent not found" parentAgentId does not exist in the registry Verify the parent ID
"Parent agent not active" Parent is Suspended, Revoked, or Terminated Reactivate parent first (requires regulator)
"Max generation depth exceeded" Child would be generation 11+ (max is 10) Cannot spawn deeper; restructure lineage
"Wallet already registered to another agent" The child wallet is already in use Generate a new key pair for the child

For the full error reference, see Errors.

Next Step

Your child agent is now registered. It should begin its own compliance loop immediately, starting with attestation scheduling.