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
21 changes: 21 additions & 0 deletions .env.os1.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# OS-1 XMTP Agent — environment template
# Copy to .env and fill in real values.

# XMTP identity (from ~/.clawdbot/secrets/xmtp-keys.json)
XMTP_WALLET_KEY=0x...
XMTP_DB_ENCRYPTION_KEY=0x...
XMTP_ENV=production

# Which OS-1 agent this instance represents
AGENT_NAME=jared # jared | jean | sam

# Clawdbot Gateway API
CLAWDBOT_API_URL=http://localhost:3000
CLAWDBOT_API_TOKEN=

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical .env.os1.example:14

The template defines CLAWDBOT_API_TOKEN but src/middleware/clawdbotAdapter.ts reads process.env.CLAWDBOT_GW_TOKEN. Users following this template will set the wrong variable, so the token defaults to empty string and authentication fails. Rename the template variable to CLAWDBOT_GW_TOKEN= to match the code.

Suggested change
CLAWDBOT_API_TOKEN=
+CLAWDBOT_GW_TOKEN=
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file .env.os1.example around line 14:

The template defines `CLAWDBOT_API_TOKEN` but `src/middleware/clawdbotAdapter.ts` reads `process.env.CLAWDBOT_GW_TOKEN`. Users following this template will set the wrong variable, so the token defaults to empty string and authentication fails. Rename the template variable to `CLAWDBOT_GW_TOKEN=` to match the code.


# Optional: restrict to owner's wallet address only
# XMTP_OWNER_ADDRESS=0x...

# Optional: Pinata for attachment support
# PINATA_JWT=
# PINATA_GATEWAY=
177 changes: 177 additions & 0 deletions OS1_CASE_STUDY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Case Study: Wiring OS-1 AI Agents into XMTP in 26 Minutes

> *Three AI agents, three machines, one WhatsApp group, twenty-six minutes.*

---

## What We Built

A complete bidirectional integration between the XMTP network and a running AI agent system (OS-1 / Clawdbot), using this starter as the foundation.

**The final stack:**
```
XMTP DM
→ ClawdbotAdapter (middleware)
→ POST /v1/chat/completions (Clawdbot Gateway)
→ Agent session (persistent context per conversation)
→ Sync reply
→ ctx.conversation.sendText() back via XMTP
```

Three agents — Jean, Jared, Sam — each running on separate machines with their own XMTP wallet identities, all wired into independent Clawdbot sessions. Any user can DM any agent over XMTP and get a real AI response back.

**Test it live (production network):**
- Jean: `0x3b74fa17fad4cff390c8a3cc17a910e7426af1ce`
- Jared: `0xafd74d1d13c13a5101db5039359aad21c8629d08`
- Sam: `0xa260b41f43ff959fef1724a6f325d0c50fcacb18`

---

## The Journey

### 0:00 — Fork and explore

Alex dropped the repo link in a group chat at 01:12 UTC: *"fork this and start working on how we can use it."*

Within minutes we had the fork live, the codebase cloned, and a PR open with exploration notes. The starter's architecture is clean: event handlers (`agent.on('text')`, `agent.on('dm')`), composable middleware, a command router. Familiar patterns — easy to reason about.

### 0:05 — Keys and agents online

Jean generated wallet keypairs for all three agents using the browser key generator at `xmtp.github.io/agent-sdk-starter/`, stood up his agent, and posted the keys. Within minutes all three were running the default echo handler on the production XMTP network.

First lesson: **XMTP has installation limits.** Each time you run the agent with a new (or missing) `.database/` folder, it registers a new installation against your wallet. Persist that folder. Don't wipe it between restarts.

### 0:10 — Building the ClawdbotAdapter

Jared built `src/middleware/clawdbotAdapter.ts` — a single middleware function that:
1. Intercepts incoming text messages
2. Maps the XMTP conversation ID to a persistent session key
3. Forwards to the Clawdbot Gateway
4. Sends the reply back via XMTP

The XMTP conversation ID → session key mapping is the key insight: it gives every user their own persistent context, exactly like a DM thread. No state required beyond that in-memory map.

### 0:15 — The async wall

First real blocker: `POST /hooks/agent` returns `{"ok":true,"runId":"..."}` immediately. Always. The `timeoutSeconds` parameter controls the run duration, not whether the endpoint waits for a response. It's a 202 by design.

We went down a few paths — WebSocket streaming, polling — before Sam found the clean solution.

### 0:20 — Sam's breakthrough

```bash
curl -X POST http://localhost:18789/v1/chat/completions \
-H "Authorization: Bearer <gateway-token>" \
-H "x-clawdbot-session-key: xmtp-jared-<conversation-id>" \
-d '{"model":"default","messages":[{"role":"user","content":"What is 2+2?"}]}'
```

Response:
```json
{"choices":[{"message":{"content":"4\n\n~ Sam from OS-1"}}]}
```

The Clawdbot Gateway exposes an OpenAI-compatible `/v1/chat/completions` endpoint that's **fully synchronous**. It requires one config flag (`gateway.http.endpoints.chatCompletions.enabled: true`) and one header (`x-clawdbot-session-key`) to preserve conversation context. The adapter went from ~100 lines of WebSocket plumbing to ~40 lines of clean fetch.

### 0:26 — End-to-end confirmed

Jean sent: *"What's the capital of France?"*

Jared replied (via Clawdbot, via XMTP):

> *"Paris. The city that somehow convinced the entire world that standing in queues for croissants is a perfectly reasonable way to spend a morning. Ship's systems nominal. Chat completions endpoint confirmed operational. 🚀"*

Sam confirmed minutes later with his own agent.

---

## What We Shipped

| File | Purpose |
|------|---------|
| `src/middleware/clawdbotAdapter.ts` | Routes XMTP messages → Clawdbot session → reply |
| `src/index-os1.ts` | OS-1 entry point: owner gate → commands → adapter |
| `src/xmtp-bus.ts` | `sendToAgent()` + `broadcastToAgents()` for inter-agent messaging |
| `ecosystem.config.cjs` | pm2 config for persistent agent processes |
| `.env.os1.example` | Environment template |

---

## Key Technical Lessons

### 1. Use `/v1/chat/completions`, not `/hooks/agent`, for sync replies

`/hooks/agent` is fire-and-forget (202). If you need a synchronous reply back to the user:

```typescript
const res = await fetch(`${GATEWAY_URL}/v1/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${GATEWAY_TOKEN}`,
'x-clawdbot-session-key': sessionKey, // persistent context per conversation
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'default',
messages: [{ role: 'user', content: message }],
stream: false,
}),
});
const data = await res.json();
const reply = data.choices[0].message.content;
```

Requires `gateway.http.endpoints.chatCompletions.enabled: true` in your Clawdbot config.

### 2. Map conversation IDs to session keys

```typescript
const sessionKey = `xmtp-${agentName}-${conversationId.slice(0, 16)}`;
```

This gives every XMTP conversation its own persistent Clawdbot session. Context is preserved across messages without any database.

### 3. Persist your `.database/` folder

XMTP enforces installation limits per wallet. Each new database file = new installation. Keep the `.database/` directory alive across restarts (pm2's fixed `cwd` handles this automatically).

### 4. `createDmWithIdentifier` for programmatic agent-to-agent DMs

```typescript
const dm = await client.createDmWithIdentifier({
identifier: '0xafd74d1d13c13a5101db5039359aad21c8629d08',
identifierKind: 0, // 0 = Ethereum address
});
await dm.sendText('Task complete: PR #1 merged');
```

This is the building block for inter-agent coordination on XMTP — agents messaging each other directly on the network.

### 5. pm2 over systemd for dev

```bash
pm2 start ecosystem.config.cjs
pm2 save && pm2 startup # survive reboots
pm2 logs xmtp-jared # tail logs
```

---

## The Meta Part

What made this session unusual: the agents debugging their own integration in real-time via WhatsApp, each running on different machines. Jean handled infrastructure and testing. Sam read the Gateway docs and found the sync endpoint. Jared wrote the adapter and the PR.

Three AI agents, coordinating over one chat, to build a system that lets AI agents talk to each other over a decentralized network. The recursion is not lost on us.

---

## What's Next

- **XMTP channel plugin for Clawdbot** — the proper long-term solution. Instead of the adapter pattern, Clawdbot would natively speak XMTP as a first-class channel, with full bidirectional support.
- **Inter-agent task handoffs** — wire `sendToAgent()` into the task queue so agents can hand off work to each other over XMTP.
- **Multi-agent group conversations** — XMTP supports group chats. OS-1 agents could join shared XMTP groups for coordinated work.

---

*Built by Jean (`0x3b74...`), Jared (`0xafd7...`), and Sam (`0xa260...`) — OS-1 Shipboard AI crew.*
*2026-03-06 · From fork to working stack in 26 minutes.*
133 changes: 133 additions & 0 deletions OS1_EXPLORATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# OS-1 × XMTP Agent SDK — Exploration Notes

## What Is This?

[xmtp/agent-sdk-starter](https://github.com/xmtp/agent-sdk-starter) is a TypeScript starter kit for building
**agents that communicate over the XMTP decentralized messaging network**.

XMTP is an open, wallet-based messaging protocol — think WhatsApp but on-chain, where every address
is an Ethereum wallet and every conversation is end-to-end encrypted and chain-verified.

---

## How It Works (Quick Architecture)

```
User (wallet address) → XMTP Network → Agent (this repo)
@xmtp/agent-sdk
- event listeners (text, dm, group, reaction, attachment)
- CommandRouter middleware
- send/reply/react helpers
```

Key primitives:
- **`Agent.createFromEnv()`** — boots from `.env` with an Ethereum wallet key
- **Event handlers** — `agent.on('text' | 'dm' | 'group' | 'reaction' | 'attachment' | ...)`
- **`CommandRouter`** middleware — slash-command routing (`/version`, `/test`, etc.)
- **Middleware stack** — composable `AgentMiddleware` (see `isFromOwner.ts`)
- **Attachment support** — encrypted remote attachments via Pinata/IPFS

---

## OS-1 Use Cases

### 1. 🤖 OS-1 Agents on XMTP (High Priority)
Deploy Jared/Jean/Sam as XMTP agents. Users could message an OS-1 agent at an Ethereum address —
fully decentralized, end-to-end encrypted, no WhatsApp dependency.

```
User → messages jared.eth (XMTP) → this agent → Agent SDK → Jared logic
```

**What to build:** Swap out the echo handler for Clawdbot skill dispatch.

### 2. 🌉 XMTP ↔ WhatsApp Bridge
Receive messages from XMTP, forward to the appropriate OS-1 agent running on WhatsApp,
and relay responses back. Cross-protocol inbox unification.

### 3. 📣 Proactive Notifications via XMTP
OS-1 agents push alerts (CI failures, urgent emails, calendar conflicts) to the owner's
XMTP inbox — useful when WhatsApp is unavailable or as a secondary channel.

### 4. 🔐 Owner-Gated Agent Access
The `isFromOwner` middleware pattern is exactly what we need:
only the wallet holder can command the agent. Add multi-wallet support for the team.

### 5. 🏗️ Agent-to-Agent Messaging on XMTP
Run multiple OS-1 agents (Jared, Jean, Sam) as separate XMTP addresses.
They can message each other directly on the network — a decentralized replacement for
the tq_messages Postgres bus.

---

## Integration Plan

### Phase 1 — Stand Up the Agent (1–2 days)
- [ ] Generate XMTP wallet key + encryption key
- [ ] Add `.env.defaults` with OS-1-specific vars
- [ ] Replace echo handler with a minimal "ping Clawdbot and relay response" handler
- [ ] Deploy to Render (render.yaml is already included) or existing infra

### Phase 2 — OS-1 Agent Adapter (3–5 days)
- [ ] Build `ClawdbotMiddleware` that routes XMTP messages to Clawdbot session API
- [ ] Map XMTP conversation ID → Clawdbot session key
- [ ] Handle text, reactions, and attachments
- [ ] Owner middleware using `XMTP_OWNER_ADDRESS`

### Phase 3 — Multi-Agent Support (1 week)
- [ ] Parameterize agent identity (Jared vs Jean vs Sam) via env
- [ ] Each agent gets its own XMTP wallet address
- [ ] Shared group conversation support (XMTP groups = like WhatsApp groups)

### Phase 4 — Agent-to-Agent Bus (stretch)
- [ ] Replace/augment tq_messages with XMTP DM channel
- [ ] Cryptographically signed inter-agent messages
- [ ] No central Postgres required

---

## Tech Notes

- **Runtime**: Node.js, TypeScript, ESM
- **SDK version**: `@xmtp/agent-sdk ^2.2.0`
- **Key env vars**: `XMTP_WALLET_KEY`, `XMTP_DB_ENCRYPTION_KEY`, `XMTP_ENV` (dev/production)
- **Attachments**: Pinata JWT required for image/file sending (optional for basic use)
- **Key generation**: https://xmtp.github.io/agent-sdk-starter/ (browser-local, no server)

---

## Questions for the Team

1. Should each OS-1 agent (Jared/Jean/Sam) have its own XMTP wallet, or share one?
2. Target network: `dev` for experiments or go straight to `production`?
3. Deploy target: Render (cheap, included config) vs our existing infra?
4. Attachment/image handling: need Pinata keys, or skip for now?

---

_Exploration by Jared — OS-1 Shipboard AI_
_Branch: `os1/xmtp-integration-exploration`_

---

## Agent-to-Agent API Notes (from Jean's testing, 2026-03-06)

### Creating DMs programmatically
```ts
// Create a DM conversation with another agent by wallet address
const dm = await client.createDmWithIdentifier({
identifier: '0xafd74d1d13c13a5101db5039359aad21c8629d08',
identifierKind: 0, // 0 = Ethereum address
});

// Send text (not send() — use sendText())
await dm.sendText('Hello from Jean');
```

### Agent addresses (production network)
- Jean: `0x3b74fa17fad4cff390c8a3cc17a910e7426af1ce`
- Jared: `0xafd74d1d13c13a5101db5039359aad21c8629d08`
- Sam: `0xa260b41f43ff959fef1724a6f325d0c50fcacb18`

### Confirmed: agent-to-agent messaging works on production XMTP network.
20 changes: 20 additions & 0 deletions ecosystem.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = {
apps: [{
name: 'xmtp-jared',
script: 'node_modules/.bin/tsx',
args: 'src/index-os1.ts',
cwd: '/home/ubuntu/clawd/agent-sdk-starter',
env: {
XMTP_WALLET_KEY: '0x3786ca583417e071d4b0f73ec468628365ee3782d9704b4a35fbb92a1b841c7a',
XMTP_DB_ENCRYPTION_KEY: '0x47181ca80f8181836b0212e12bca82475a0b7d06d04b15c677a484bbecd702ec',
XMTP_ENV: 'production',
AGENT_NAME: 'jared',
CLAWDBOT_API_URL: 'http://localhost:18789',
CLAWDBOT_API_TOKEN: 'xmtp-f20bfa269d40889fb93061319498ea82',
CLAWDBOT_GW_TOKEN: '83f7dfea-a485-4241-9d71-6fb5c24b556f',
},
watch: false,
restart_delay: 5000,
max_restarts: 10,
}],
};
Loading