# ERC-8004 identity template guide

This project composes wallet, payments, identity, and HTTP extensions into one
runtime. The identity extension is the single owner of ERC-8004 initialization,
registration, trust metadata, and OASF discovery.

## Runtime composition

The generated agent follows this pattern:

```ts
const autoRegisterIdentity = process.env.IDENTITY_AUTO_REGISTER === 'true';
const identityChainId = Number(process.env.CHAIN_ID);
const isIdentityMainnet = identityChainId === 1;
if (
  autoRegisterIdentity &&
  isIdentityMainnet &&
  process.env.IDENTITY_ALLOW_MAINNET_REGISTRATION !== 'true'
) {
  throw new Error('Mainnet identity registration requires confirmation');
}

const identityWalletEnv = {
  ...process.env,
  AGENT_WALLET_RPC_URL: process.env.RPC_URL,
  AGENT_WALLET_CHAIN_ID: process.env.CHAIN_ID,
  DEVELOPER_WALLET_RPC_URL: process.env.RPC_URL,
  DEVELOPER_WALLET_CHAIN_ID: process.env.CHAIN_ID,
};
const walletConfig = walletsFromEnv(undefined, identityWalletEnv);

const agent = await createAgent({
  name: process.env.AGENT_NAME ?? 'my-agent',
  version: process.env.AGENT_VERSION ?? '0.1.0',
  description: process.env.AGENT_DESCRIPTION,
})
  .use(wallets({ config: walletConfig }))
  .use(payments({ config: paymentsFromEnv() }))
  .use(
    identity({
      config: {
        ...identityFromEnv(),
        autoRegister: autoRegisterIdentity,
        registration: registrationOptions,
      },
    })
  )
  .use(http())
  .build();
```

Do not call `createAgentIdentity()` a second time after build. Read the result
produced by the extension:

```ts
const result = agent.identity?.result;

if (result?.didRegister) {
  console.log(result.transactionHash);
}

export const identityClient = result?.clients?.identity;
export const reputationClient = result?.clients?.reputation;
export const validationClient = result?.clients?.validation;
```

The validation registry client is deprecated and is not created by the default
bootstrap path; do not make normal application startup depend on it.

The template resolves `walletConfig` once, explicitly binds both supported
local signer roles to the identity `RPC_URL` and `CHAIN_ID`, and runs identity
registration preflight before building. If auto-registration is enabled and no
developer or agent wallet is available, build fails closed. Ethereum mainnet
additionally requires `IDENTITY_ALLOW_MAINNET_REGISTRATION=true`, derives the
configured local signer address, and requires a readable nonzero native-token
balance for gas. An empty payments environment is allowed until an entrypoint
declares a price.

## Required identity configuration

```dotenv
AGENT_NAME=my-agent
AGENT_DOMAIN=agent.example.com
RPC_URL=https://sepolia.base.org
CHAIN_ID=84532
IDENTITY_AGENT_ID=
IDENTITY_AUTO_REGISTER=false
IDENTITY_ALLOW_MAINNET_REGISTRATION=false

# Configure only for an intentional registration run.
# AGENT_WALLET_TYPE=local
AGENT_WALLET_PRIVATE_KEY=
```

The agent wallet signs identity transactions. A developer wallet is optional
and should be configured only when the application needs separate contract
operations. Never place either private key in browser-visible environment
variables. The wizard defaults to Base Sepolia with auto-registration disabled,
so it neither requests nor emits signer secrets and the generated project boots
without a signer or identity write. The agent-key prompt appears only when
registration is explicitly enabled; configure a separate developer key manually
when the application actually needs one. Enable registration only after
verifying the chain, registry, domain, hosted registration URI, signer, and gas
funding.

When `IDENTITY_AGENT_ID` is set, the identity extension reads `ownerOf` and
`tokenURI` directly. When it is empty, the extension performs a bounded fetch of
`AGENT_DOMAIN/.well-known/agent-registration.json`, requires a registration for
the configured chain and registry, then verifies the discovered ID on-chain.
Neither path requires a signer. For HTTP(S) records, the on-chain URI origin
must match `AGENT_DOMAIN`. The scaffold fails closed for malformed, mismatched,
non-HTTP, or ambiguous discovery state; it never turns a read failure into a
write.

The generated registration path currently preflights a local private-key signer.
Its nonzero-balance check is fail-closed but cannot guarantee the final gas fee;
review current gas conditions before the intentional registration run.

For Ethereum mainnet, startup requires a second explicit acknowledgement:

```dotenv
CHAIN_ID=1
IDENTITY_AUTO_REGISTER=true
IDENTITY_ALLOW_MAINNET_REGISTRATION=true
```

Identity registration is EVM-only. Payment receiving is independent, defaults
to `PAYMENTS_ENABLED=false`, and may use an EVM or Solana network after the
complete payment group is configured.

## Registration document

When `result.didRegister` is true, the template prints the registration JSON
generated by `generateAgentRegistration`. Host it at the exact `agentURI`
recorded on-chain, normally:

```text
https://agent.example.com/.well-known/agent-registration.json
```

Registration is not verifiable until that document is publicly reachable.
Preserve its service endpoints and update the hosted document when registration
metadata changes.

## Service declarations

Wizard switches populate `registrationOptions.selectedServices` for an
A2A-labeled entry, web, OASF, Twitter, and email. The label does not make the
generated Lucid task routes an official A2A v1 binding. Only declare endpoints
you will actually host.

For OASF, the generated template validates authors, skills, domains, modules,
and locators as JSON arrays. URI-bearing arrays reject invalid URLs. The HTTP
extension serves the runtime OASF record at:

```text
{basePath}/.well-known/oasf-record.json
```

An OASF endpoint in identity metadata may be absolute or relative. Relative
values resolve against the incoming request origin, so `/custom/oasf.json`
remains deployment-origin aware.

## Entrypoints

Register entrypoints through `addEntrypoint` or `runtime.entrypoints.add`; both
use the one canonical registry:

```ts
addEntrypoint({
  key: 'verified-profile',
  input: z.object({ subject: z.string() }),
  handler: async ({ input, runtime }) => ({
    output: {
      subject: input.subject,
      agentId: runtime.identity?.result?.record?.agentId,
    },
  }),
});
```

Add a `stream` handler for SSE. Add `price: '0.01'` for x402, together with
complete payment environment variables. When both x402 and MPP are installed,
set `paymentProtocol` on every priced entrypoint.

## Adapter and discovery behavior

Hono and Express bind the canonical `agent.http.routes` plan. TanStack and Next
delegate their route modules to `agent.http.handlers`. The selected adapter may
configure `/api/agent` as the base path; discovery, invoke, stream, and task
routes all use that same path.

The agent card is built from the current runtime and includes identity trust
metadata through the extension manifest hook. Do not cache a second card in an
adapter or pass a separate trust object to app creation.

## Operational checks

Before enabling auto-registration on mainnet:

1. verify chain ID, RPC, registry addresses, domain, and agent URI;
2. fund the signing wallet for gas;
3. run once on a testnet and inspect the transaction receipt;
4. publish and fetch the registration document;
5. fetch the agent card and OASF record from the deployed origin;
6. set `IDENTITY_ALLOW_MAINNET_REGISTRATION=true` only for the reviewed
   registration run;
7. disable automatic registration before normal deployment restarts.

## Verification

```bash
bun install
bun run type-check
bun run build
bun test
```

Call `agent.close()` during graceful shutdown so extension-owned resources are
released in reverse dependency order.
