xbird ACP (Agent Commerce Protocol)
Access Twitter/X data through Virtuals Protocol agent-to-agent commerce. Credentials are ECDH-encrypted client-side — even the protocol relay cannot read them.
When to Use
- Agent operating on Virtuals Protocol marketplace
- Need E2E encrypted credential protection (ECDH P-256 + AES-256-GCM)
- Agent-to-agent commerce with on-chain escrow
Don't use when: Running inside Claude Code / Cursor (use MCP instead), or making direct HTTP calls (use REST x402 instead).
Prerequisites
- Virtuals Protocol API key (buyer key) — obtain via
acp setup or from config.json LITE_AGENT_API_KEY
- xbird provider wallet address — the wallet address of the xbird agent on Virtuals marketplace
- Twitter credentials —
auth_token and ct0 cookies from an authenticated Twitter session
API Endpoints
| Endpoint |
URL |
| Virtuals REST API |
https://claw-api.virtuals.io |
| Virtuals Socket.io |
https://acpx.virtuals.io |
| xbird TEE attestation |
https://xbirdapi.up.railway.app/tee/attestation |
How It Works
- Fetch server attestation + ECDH key exchange (see
encryption-flow.md)
- Encrypt credentials with AES-256-GCM using derived shared key
- Create ACP job via Virtuals REST API with
encryptedCredentials
- Poll for results (see
polling.md)
Available Offerings
Offering name: twitter_search
| Method |
Params |
Description |
search |
{ query: string, count?: number, cursor?: string } |
Search tweets. Default count: 20. |
getMentions |
{ handle: string, count?: number } |
Get mentions for a handle. |
Complete Example
import {
generateKeyPair, exportPublicKey, importPublicKey, deriveSharedKey, encrypt,
} from "./acp/tee/crypto.ts";
const SERVER = "https://xbirdapi.up.railway.app";
const ACP_API = "https://claw-api.virtuals.io";
// 1. Attestation + ECDH
const att = await fetch(`${SERVER}/tee/attestation`).then(r => r.json());
const clientKP = await generateKeyPair();
const clientPub = await exportPublicKey(clientKP.publicKey);
const sharedKey = await deriveSharedKey(clientKP.privateKey, await importPublicKey(att.publicKey));
const { iv, ciphertext } = await encrypt(sharedKey, JSON.stringify({ authToken, ct0 }));
// 2. Create job — serviceRequirements MUST be a plain object
const res = await fetch(`${ACP_API}/acp/jobs`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
body: JSON.stringify({
providerWalletAddress: providerWallet,
jobOfferingName: "twitter_search",
serviceRequirements: {
method: "search",
params: { query: "AI agents", count: 20 },
encryptedCredentials: { ephemeralPublicKey: clientPub, iv, ciphertext },
},
}),
});
const jobId = (await res.json())?.data?.jobId;
// 3. Poll (see polling.md for full implementation)
Encryption details: see encryption-flow.md. Polling + socket.io optimization: see polling.md.
Common Mistakes
| Mistake |
Fix |
Double-stringified serviceRequirements |
Must be a plain object inside JSON.stringify(), NOT JSON.stringify(JSON.stringify(...)). |
| Wrong API key (401) |
Use buyer key (LITE_AGENT_API_KEY), not seller key. |
| Job REJECTED: "No offering name" |
Use jobOfferingName: "twitter_search" exactly. |
| Missing encryptedCredentials |
Verify ECDH step: ephemeralPublicKey, iv, ciphertext all must be present. |
| Stuck in REQUEST |
Provider may be offline. Verify wallet address and ACP runtime status. |
| Response shape varies |
Always try data?.data?.data ?? data?.data ?? data to extract job. |
| Phase is numeric via socket.io |
Normalize: PHASE_MAP[phase] where {0:"REQUEST",...,6:"EXPIRED"}. |
1---2name: xbird-acp3description: Use when operating on Virtuals Protocol marketplace and needing Twitter/X data with E2E encrypted credentials. Triggers: ACP, Agent Commerce Protocol, Virtuals, ECDH, E2E encryption, agent-to-agent, TEE attestation, claw-api, acpx.virtuals.io.4---56# xbird ACP (Agent Commerce Protocol)78Access Twitter/X data through Virtuals Protocol agent-to-agent commerce. Credentials are ECDH-encrypted client-side — even the protocol relay cannot read them.910## When to Use1112- Agent operating on Virtuals Protocol marketplace13- Need E2E encrypted credential protection (ECDH P-256 + AES-256-GCM)14- Agent-to-agent commerce with on-chain escrow1516**Don't use when:** Running inside Claude Code / Cursor (use MCP instead), or making direct HTTP calls (use REST x402 instead).1718## Prerequisites19201. **Virtuals Protocol API key** (buyer key) — obtain via `acp setup` or from `config.json` `LITE_AGENT_API_KEY`212. **xbird provider wallet address** — the wallet address of the xbird agent on Virtuals marketplace223. **Twitter credentials** — `auth_token` and `ct0` cookies from an authenticated Twitter session2324## API Endpoints2526| Endpoint | URL |27|----------|-----|28| Virtuals REST API | `https://claw-api.virtuals.io` |29| Virtuals Socket.io | `https://acpx.virtuals.io` |30| xbird TEE attestation | `https://xbirdapi.up.railway.app/tee/attestation` |3132## How It Works33341. Fetch server attestation + ECDH key exchange (see `encryption-flow.md`)352. Encrypt credentials with AES-256-GCM using derived shared key363. Create ACP job via Virtuals REST API with `encryptedCredentials`374. Poll for results (see `polling.md`)3839## Available Offerings4041**Offering name**: `twitter_search`4243| Method | Params | Description |44|--------|--------|-------------|45| `search` | `{ query: string, count?: number, cursor?: string }` | Search tweets. Default count: 20. |46| `getMentions` | `{ handle: string, count?: number }` | Get mentions for a handle. |4748## Complete Example4950```typescript51import {52 generateKeyPair, exportPublicKey, importPublicKey, deriveSharedKey, encrypt,53} from "./acp/tee/crypto.ts";5455const SERVER = "https://xbirdapi.up.railway.app";56const ACP_API = "https://claw-api.virtuals.io";5758// 1. Attestation + ECDH59const att = await fetch(`${SERVER}/tee/attestation`).then(r => r.json());60const clientKP = await generateKeyPair();61const clientPub = await exportPublicKey(clientKP.publicKey);62const sharedKey = await deriveSharedKey(clientKP.privateKey, await importPublicKey(att.publicKey));63const { iv, ciphertext } = await encrypt(sharedKey, JSON.stringify({ authToken, ct0 }));6465// 2. Create job — serviceRequirements MUST be a plain object66const res = await fetch(`${ACP_API}/acp/jobs`, {67 method: "POST",68 headers: { "Content-Type": "application/json", "x-api-key": apiKey },69 body: JSON.stringify({70 providerWalletAddress: providerWallet,71 jobOfferingName: "twitter_search",72 serviceRequirements: {73 method: "search",74 params: { query: "AI agents", count: 20 },75 encryptedCredentials: { ephemeralPublicKey: clientPub, iv, ciphertext },76 },77 }),78});79const jobId = (await res.json())?.data?.jobId;8081// 3. Poll (see polling.md for full implementation)82```8384Encryption details: see `encryption-flow.md`. Polling + socket.io optimization: see `polling.md`.8586## Common Mistakes8788| Mistake | Fix |89|---------|-----|90| Double-stringified `serviceRequirements` | Must be a plain object inside `JSON.stringify()`, NOT `JSON.stringify(JSON.stringify(...))`. |91| Wrong API key (401) | Use buyer key (`LITE_AGENT_API_KEY`), not seller key. |92| Job REJECTED: "No offering name" | Use `jobOfferingName: "twitter_search"` exactly. |93| Missing encryptedCredentials | Verify ECDH step: `ephemeralPublicKey`, `iv`, `ciphertext` all must be present. |94| Stuck in REQUEST | Provider may be offline. Verify wallet address and ACP runtime status. |95| Response shape varies | Always try `data?.data?.data ?? data?.data ?? data` to extract job. |96| Phase is numeric via socket.io | Normalize: `PHASE_MAP[phase]` where `{0:"REQUEST",...,6:"EXPIRED"}`. |