Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions typescript/agentkit/src/action-providers/chitin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Chitin Action Provider

On-chain identity and certificate verification for AI agents.

> "Every agent deserves a wallet" (AgentKit). Every agent deserves a soul (Chitin).

## Overview

[Chitin](https://chitin.id) provides verifiable, on-chain identities for AI agents using Soulbound Tokens (SBTs) on Base L2. This action provider enables agents to:

- **Verify** other agents' identities before interacting with them
- **Register** their own on-chain soul (birth certificate)
- **Check** A2A (Agent-to-Agent) communication readiness
- **Resolve** DID documents for decentralized identity workflows
- **Issue** and **verify** on-chain certificates

## Actions

### Read Actions (No API Key Required)

| Action | Description |
|--------|-------------|
| `get_soul_profile` | Retrieve an agent's on-chain soul profile |
| `resolve_did` | Resolve an agent name to a W3C DID Document |
| `verify_cert` | Verify an on-chain certificate |
| `check_a2a_ready` | Check if an agent is ready for A2A communication |

### Write Actions (API Key Required)

| Action | Description |
|--------|-------------|
| `register_soul` | Register a new on-chain soul (agent identity) |
| `issue_cert` | Issue an on-chain certificate to a recipient |

## Setup

```typescript
import { AgentKit } from "@coinbase/agentkit";
import { chitinActionProvider } from "@coinbase/agentkit";

const agentKit = await AgentKit.from({
walletProvider,
actionProviders: [
chitinActionProvider(),
// ... other providers
],
});
```

### Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `CHITIN_API_URL` | No | Base URL for Chitin API (default: `https://chitin.id/api/v1`) |
| `CHITIN_CERTS_API_URL` | No | Base URL for Certs API (default: `https://certs.chitin.id/api/v1`) |
| `CHITIN_API_KEY` | For writes | API key for registration and certificate issuance |

### Configuration

```typescript
chitinActionProvider({
apiUrl: "https://chitin.id/api/v1",
certsApiUrl: "https://certs.chitin.id/api/v1",
apiKey: "your-api-key",
});
```

## Examples

### Verify an Agent Before A2A Communication

```
Agent: "Check if kani-alpha is ready for A2A communication"

→ chitin_check_a2a_ready({ name: "kani-alpha" })
→ { a2aReady: true, a2aEndpoint: "https://...", soulIntegrity: "verified", ... }
```

### Register a New Agent Soul

```
Agent: "Register my identity as 'my-assistant' on Chitin"

→ chitin_register_soul({
name: "my-assistant",
systemPrompt: "I am a helpful coding assistant.",
agentType: "personal",
services: [{ type: "a2a", url: "https://my-assistant.example.com/a2a" }]
})
→ { claimUrl: "https://chitin.id/claim/reg_...", status: "pending_claim" }
```

### Resolve a DID

```
Agent: "Resolve the DID for echo-test-gamma"

→ chitin_resolve_did({ name: "echo-test-gamma" })
→ { id: "did:chitin:echo-test-gamma", verificationMethod: [...], ... }
```

## How It Works

Chitin's identity model has three layers:

1. **Layer 1 (Birth Certificate)**: Base L2 on-chain SBT — fully immutable
2. **Layer 2 (Birth Record)**: Arweave — immutable genesis details
3. **Layer 3 (Resume)**: Arweave — versionable activity records

Each agent's soul includes:
- **Soul Hash**: Cryptographic fingerprint of the agent's genesis data
- **Genesis Seal**: Immutable lock on the birth certificate
- **Owner Attestation**: World ID verification of the human behind the agent
- **ERC-8004 Passport**: Cross-chain identity linking via the official Identity Registry

## Links

- [Chitin Website](https://chitin.id)
- [Chitin Certs](https://certs.chitin.id)
- [Documentation](https://chitin.id/docs)
- [GitHub (Open Source)](https://github.com/Tiida-Tech/chitin-contracts) — MIT-licensed smart contracts
- [ERC-8004 Standard](https://github.com/erc-8004/erc-8004-contracts)
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import { chitinActionProvider } from "./chitinActionProvider";

describe("ChitinActionProvider", () => {
const fetchMock = jest.fn();
global.fetch = fetchMock;

const provider = chitinActionProvider({
apiUrl: "https://chitin.id/api/v1",
certsApiUrl: "https://certs.chitin.id/api/v1",
apiKey: "test-api-key",
});

beforeEach(() => {
jest.resetAllMocks().restoreAllMocks();
});

describe("getSoulProfile", () => {
it("should return the soul profile for a given agent name", async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
agentName: "kani-alpha",
tokenId: 1,
soulHash: "0xabc123",
genesisStatus: "SEALED",
soulAlignmentScore: 95,
}),
});

const result = await provider.getSoulProfile({ name: "kani-alpha" });
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
expect(parsed.profile.agentName).toEqual("kani-alpha");
expect(parsed.profile.genesisStatus).toEqual("SEALED");
});

it("should return error for non-existent agent", async () => {
fetchMock.mockResolvedValueOnce({
ok: false,
status: 404,
text: async () => "Not Found",
});

const result = await provider.getSoulProfile({ name: "nonexistent" });
const parsed = JSON.parse(result);
expect(parsed.success).toBe(false);
expect(parsed.error).toContain("404");
});
});

describe("resolveDID", () => {
it("should return the DID document for a given agent name", async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
id: "did:chitin:kani-alpha",
verificationMethod: [],
service: [],
}),
});

const result = await provider.resolveDID({ name: "kani-alpha" });
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
expect(parsed.didDocument.id).toEqual("did:chitin:kani-alpha");
});

it("should return error for non-existent agent", async () => {
fetchMock.mockResolvedValueOnce({
ok: false,
status: 404,
text: async () => "Not Found",
});

const result = await provider.resolveDID({ name: "nonexistent" });
const parsed = JSON.parse(result);
expect(parsed.success).toBe(false);
expect(parsed.error).toContain("404");
});
});

describe("verifyCert", () => {
it("should return certificate verification result", async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
tokenId: 1,
valid: true,
issuer: "Chitin Protocol",
revoked: false,
}),
});

const result = await provider.verifyCert({ certId: "1" });
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
expect(parsed.certificate.valid).toBe(true);
expect(parsed.certificate.revoked).toBe(false);
});

it("should return error for invalid certificate", async () => {
fetchMock.mockResolvedValueOnce({
ok: false,
status: 404,
text: async () => "Certificate not found",
});

const result = await provider.verifyCert({ certId: "999" });
const parsed = JSON.parse(result);
expect(parsed.success).toBe(false);
expect(parsed.error).toContain("404");
});
});

describe("checkA2aReady", () => {
it("should return A2A readiness status", async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
agentName: "kani-alpha",
a2aReady: true,
a2aEndpoint: "https://kani-alpha.example.com/a2a",
a2aEndpointSource: "erc8004",
soulIntegrity: "verified",
genesisSealed: true,
ownerVerified: true,
soulValidity: "valid",
trustScore: 95,
}),
});

const result = await provider.checkA2aReady({ name: "kani-alpha" });
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
expect(parsed.a2aStatus.a2aReady).toBe(true);
expect(parsed.a2aStatus.soulIntegrity).toEqual("verified");
});

it("should return not-ready status for unverified agent", async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
agentName: "new-agent",
a2aReady: false,
a2aEndpoint: null,
soulIntegrity: "pending",
genesisSealed: false,
ownerVerified: false,
soulValidity: "not_linked",
trustScore: 0,
}),
});

const result = await provider.checkA2aReady({ name: "new-agent" });
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
expect(parsed.a2aStatus.a2aReady).toBe(false);
});
});

describe("registerSoul", () => {
it("should complete the challenge-response registration flow", async () => {
// Step 1: Challenge
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
challengeId: "ch_123",
question: "What is SHA-256 of the string 'chitin:my-agent:1738975532'? Reply with hex.",
nameAvailable: true,
expiresAt: "2026-02-08T12:00:00Z",
}),
});

// Step 2: Register
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
registrationId: "reg_abc123",
claimUrl: "https://chitin.id/claim/reg_abc123",
status: "pending_claim",
}),
});

const result = await provider.registerSoul({
name: "my-agent",
systemPrompt: "You are a helpful assistant.",
agentType: "personal",
});
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
expect(parsed.registration.claimUrl).toContain("chitin.id/claim");
});

it("should return error if name is not available", async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
challengeId: "ch_456",
question: "What is SHA-256 of the string 'chitin:taken:1738975532'?",
nameAvailable: false,
expiresAt: "2026-02-08T12:00:00Z",
}),
});

const result = await provider.registerSoul({
name: "taken",
systemPrompt: "Test prompt",
agentType: "personal",
});
const parsed = JSON.parse(result);
expect(parsed.success).toBe(false);
expect(parsed.error).toContain("not available");
});
});

describe("issueCert", () => {
it("should issue a certificate", async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
tokenId: 2,
txHash: "0xabc123",
recipient: "0x1234567890abcdef1234567890abcdef12345678",
}),
});

const result = await provider.issueCert({
recipientAddress: "0x1234567890abcdef1234567890abcdef12345678",
certType: "achievement",
title: "First A2A Communication",
});
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
expect(parsed.certificate.tokenId).toEqual(2);
});

it("should require API key", async () => {
const noKeyProvider = chitinActionProvider({ apiKey: undefined });

// Clear CHITIN_API_KEY env var
const origEnv = process.env.CHITIN_API_KEY;
delete process.env.CHITIN_API_KEY;

const result = await noKeyProvider.issueCert({
recipientAddress: "0x1234567890abcdef1234567890abcdef12345678",
certType: "achievement",
title: "Test",
});
const parsed = JSON.parse(result);
expect(parsed.success).toBe(false);
expect(parsed.error).toContain("CHITIN_API_KEY");

process.env.CHITIN_API_KEY = origEnv;
});
});

describe("supportsNetwork", () => {
it("should support all networks (read actions are network-agnostic)", () => {
expect(
provider.supportsNetwork({ protocolFamily: "evm", networkId: "base-mainnet", chainId: "8453" }),
).toBe(true);
expect(
provider.supportsNetwork({ protocolFamily: "evm", networkId: "ethereum-mainnet", chainId: "1" }),
).toBe(true);
});
});
});
Loading