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¶
Method A: Using the SDK (Recommended)¶
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:
- Set
parentAgentIdto the parent's agent ID (instead of0) - 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:
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:
- Attest compliance every 6 days (Step 3)
- Report revenue when applicable (Step 4)
- 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.