# Farcaster Registration API

This API registers Farcaster accounts on Optimism and guides you through the complete setup: on-chain registration, profile configuration, username, and first cast.

It handles two on-chain transactions:
1. `IdGateway.registerFor()` — registers an FID for a custody address
2. `KeyGateway.addFor()` — adds an Ed25519 signer key to that FID

## Agent Onboarding Guide

**IMPORTANT: Before making any API calls, have this conversation with your user.** Collecting all preferences upfront lets you execute the entire flow (registration + profile + first cast) without interruption.

### Questions to ask your user:

**1. Username (fname)**
Ask: "What Farcaster username do you want? (lowercase letters, numbers, hyphens, 1-16 chars)"
- Check availability: `GET /v1/farcaster/fname/{name}/available`
- If taken, suggest variations (e.g. append numbers, shorten) and re-check
- Warn: fname changes have a 28-day cooldown, so choose carefully

**2. Profile details**
Ask: "What display name, bio, and profile picture URL do you want?"
- Display name: max 32 bytes UTF-8 (suggest their real name or project name)
- Bio: max 256 bytes UTF-8
- Profile picture: URL to an image (if none, you can skip or suggest a default)
- URL: optional website link

**3. Welcome post**
Ask: "I'll introduce you in the /agents channel on Farcaster. Want me to? What should it say?"
- Default: yes
- Suggested template: "gm! I'm {display_name}. {one sentence about what they do}. Just registered on Farcaster via fid-forge."
- Confirm the exact text with the user before posting

**4. Key storage**
Ask: "Where should I save your Farcaster keys? I recommend a .env file."
- Get the file path from the user
- Keys to save: custody private key, signer private key, signer public key, FID

Once you have all this info, proceed with the registration flow below.

## Quick Start

### Programmatic Flow (MPP / Tempo stablecoins)

```
POST /v1/farcaster/payments/mpp/registration
  Body: { "address": "0x...", "signerPubKey": "0x..." }
  → 402 with WWW-Authenticate: Payment <challenge>
  → Retry with Authorization: Payment <credential>
  → 201 { registrationId, status: "AWAITING_SIGNATURES" }

[Continue with signing flow below]
```

### Programmatic Flow (x402)

```
POST /v1/farcaster/payments/x402/registration
  Body: { "address": "0x...", "signerPubKey": "0x..." }
  → 402 with PAYMENT-REQUIRED header
  → Retry with PAYMENT-SIGNATURE header
  → 201 { registrationId, status: "AWAITING_SIGNATURES" }

GET /v1/farcaster/registrations/{registrationId}/instructions
  → 200 { typedData: { register: {...}, add: {...} }, deadline }

POST /v1/farcaster/registrations/{registrationId}/signatures
  Body: { "registerSignature": "0x...", "addSignature": "0x..." }
  → 202 { status: "SUBMITTED_ONCHAIN" }

GET /v1/farcaster/registrations/{registrationId}
  → Poll until status: "COMPLETED"
  → { fid: 12345, tx: { register: "0x...", addKey: "0x..." } }
```

### Human-in-the-Loop Flow (Stripe)

```
POST /v1/farcaster/payments/stripe/registration
  Body: { "address": "0x...", "signerPubKey": "0x..." }
  → 201 { registrationId, payment: { checkoutUrl: "https://checkout.stripe.com/..." } }

[User completes Stripe checkout in browser]

GET /v1/farcaster/registrations/{registrationId}
  → Poll until status: "AWAITING_SIGNATURES"

[Continue with signing flow above]
```

## Endpoints

### GET /v1/farcaster/fname/{name}/available

Check if a Farcaster username (fname) is available. Use this during the interactive setup phase before registration.

Request:
- URL parameter `name`: lowercase alphanumeric + hyphens, 1-16 chars

Response (200):
```json
{
  "name": "my-agent",
  "available": true
}
```

### POST /v1/farcaster/payments/stripe/registration

Create a registration with Stripe checkout.

Request:
```json
{
  "address": "0xAbC123...",
  "signerPubKey": "0x<64 hex chars>",
  "recoveryAddress": "0x..."
}
```

Headers:
- `Content-Type: application/json` (required)
- `Idempotency-Key: <string>` (optional, prevents duplicate registrations)

Response (201):
```json
{
  "registrationId": "reg_...",
  "status": "AWAITING_PAYMENT",
  "payment": {
    "method": "stripe",
    "status": "unpaid",
    "checkoutUrl": "https://checkout.stripe.com/..."
  },
  "next": {
    "poll": "/v1/farcaster/registrations/reg_.../",
    "afterPaid": "/v1/farcaster/registrations/reg_.../instructions"
  }
}
```

### POST /v1/farcaster/payments/mpp/registration

Create a registration with MPP (Machine Payments Protocol) using Tempo stablecoins. Uses a two-step protocol:
1. First call (no Authorization header): returns 402 with WWW-Authenticate challenge
2. Second call (with Authorization: Payment <credential>): verifies payment and creates registration

Request body is identical to the Stripe endpoint.

Additional header on retry:
- `Authorization: Payment <base64-encoded-credential>`

Response (201):
```json
{
  "registrationId": "reg_...",
  "status": "AWAITING_SIGNATURES",
  "payment": {
    "method": "mpp",
    "status": "paid"
  },
  "next": {
    "instructions": "/v1/farcaster/registrations/reg_.../instructions",
    "poll": "/v1/farcaster/registrations/reg_..."
  }
}
```

### POST /v1/farcaster/payments/x402/registration

Create a registration with x402 payment. Uses a two-step protocol:
1. First call (no PAYMENT-SIGNATURE): returns 402 with challenge
2. Second call (with PAYMENT-SIGNATURE): processes payment and creates registration

Request body is identical to the Stripe endpoint.

Additional headers on retry:
- `PAYMENT-SIGNATURE: <x402 payment proof>`

Response (201):
```json
{
  "registrationId": "reg_...",
  "status": "AWAITING_SIGNATURES",
  "payment": {
    "method": "x402",
    "status": "paid",
    "network": "eip155:8453"
  },
  "next": {
    "instructions": "/v1/farcaster/registrations/reg_.../instructions",
    "poll": "/v1/farcaster/registrations/reg_..."
  }
}
```

### GET /v1/farcaster/registrations/{registrationId}

Poll registration status.

Response (200):
```json
{
  "registrationId": "reg_...",
  "status": "COMPLETED",
  "address": "0x...",
  "recoveryAddress": "0x...",
  "signerPubKey": "0x...",
  "fid": 12345,
  "tx": {
    "register": "0x<tx hash>",
    "addKey": "0x<tx hash>"
  },
  "error": null
}
```

When status is FAILED:
```json
{
  "registrationId": "reg_...",
  "status": "FAILED",
  "error": { "code": "OP_TX_REVERT", "message": "..." }
}
```

### GET /v1/farcaster/registrations/{registrationId}/instructions

Get EIP-712 typed data for signing. Only available when status is `AWAITING_SIGNATURES`.

Response (200):
```json
{
  "registrationId": "reg_...",
  "instructions": "1) Sign Register typed data. 2) Sign Add typed data. 3) POST both...",
  "deadline": 1234567890,
  "typedData": {
    "register": {
      "domain": { "name": "Farcaster IdGateway", "version": "1", "chainId": 10, "verifyingContract": "0x..." },
      "types": { "Register": [{ "name": "to", "type": "address" }, ...] },
      "primaryType": "Register",
      "message": { "to": "0x...", "recovery": "0x...", "nonce": "42", "deadline": "1234567890" }
    },
    "add": {
      "domain": { "name": "Farcaster KeyGateway", "version": "1", "chainId": 10, "verifyingContract": "0x..." },
      "types": { "Add": [{ "name": "owner", "type": "address" }, ...] },
      "primaryType": "Add",
      "message": { "owner": "0x...", "key": "0x...", "keyType": 1, "metadata": "0x...", "metadataType": 1, "nonce": "42", "deadline": "1234567890" }
    }
  },
  "metadata": "0x..."
}
```

Note: All bigint values (nonce, deadline) are serialized as strings in JSON.

### POST /v1/farcaster/registrations/{registrationId}/signatures

Submit EIP-712 signatures. Both must be signed by the custody address.

Request:
```json
{
  "registerSignature": "0x<hex signature>",
  "addSignature": "0x<hex signature>"
}
```

Response (202):
```json
{
  "registrationId": "reg_...",
  "status": "SUBMITTED_ONCHAIN",
  "next": { "poll": "/v1/farcaster/registrations/reg_..." }
}
```

## State Machine

```
AWAITING_PAYMENT ─(Stripe webhook)─→ AWAITING_SIGNATURES ─(signatures posted)─→ SUBMITTED_ONCHAIN ─→ COMPLETED
                                     ↑ (x402: starts here)                                        ─→ FAILED
                                                                                                   ─→ EXPIRED
```

Terminal states: COMPLETED, FAILED, EXPIRED.

## Input Requirements

- **address**: Valid EVM address, 0x-prefixed, 20 bytes. Will be checksummed server-side.
- **signerPubKey**: Ed25519 public key, 0x + exactly 64 hex characters (32 bytes).
- **recoveryAddress**: Optional. Defaults to 0x0000000000000000000000000000000000000000.
- **registerSignature / addSignature**: Hex-encoded EIP-712 signatures from the custody wallet.

## Signing Rules

1. Use the custody wallet (same address submitted during registration) to sign both payloads.
2. Sign the exact `typedData.register` and `typedData.add` objects from the /instructions response.
3. Do NOT modify any fields before signing.
4. If the deadline expires, call GET /instructions again for fresh typed data and re-sign.
5. Submit signatures as 0x-prefixed hex strings.

## EIP-712 Signing with viem

```typescript
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { optimism } from "viem/chains";

const account = privateKeyToAccount("0x<custody_private_key>");

// Get typed data from /instructions endpoint
const { typedData } = instructionsResponse;

const registerSignature = await account.signTypedData({
  domain: typedData.register.domain,
  types: typedData.register.types,
  primaryType: typedData.register.primaryType,
  message: typedData.register.message,
});

const addSignature = await account.signTypedData({
  domain: typedData.add.domain,
  types: typedData.add.types,
  primaryType: typedData.add.primaryType,
  message: typedData.add.message,
});
```

## EIP-712 Signing with ethers.js

```typescript
import { Wallet } from "ethers";

const wallet = new Wallet("0x<custody_private_key>");

const registerSignature = await wallet.signTypedData(
  typedData.register.domain,
  typedData.register.types,
  typedData.register.message
);

const addSignature = await wallet.signTypedData(
  typedData.add.domain,
  typedData.add.types,
  typedData.add.message
);
```

## Polling Strategy

- After Stripe checkout: poll every 3-5 seconds until `AWAITING_SIGNATURES`.
- After signature submission: poll every 2-5 seconds until terminal state.
- Terminal states: `COMPLETED`, `FAILED`, `EXPIRED`.
- On `COMPLETED`: read `fid` and `tx` from response.

## Idempotency

Send `Idempotency-Key` header on payment creation endpoints to safely retry on network errors.
Same key + same payment method returns the existing registration without re-charging.

## Networks

- **Farcaster contracts**: Optimism (chainId 10)
- **x402 payments**: Base mainnet (CAIP-2: eip155:8453)

## Rate Limits

- 30 requests per minute per IP address
- 5 requests per minute per EVM address
- Applies to payment creation endpoints only

## Error Response Format

All errors follow this shape:
```json
{ "error": { "code": "ERROR_CODE", "message": "Human-readable description", "details": {} } }
```

## Error Codes and Recovery

| Code | HTTP | Recovery |
|------|------|----------|
| ADDRESS_ALREADY_HAS_FID | 400 | Use a different custody address |
| REGISTRATION_NOT_FOUND | 404 | Verify the registrationId |
| PAYMENT_REQUIRED | 402/409 | Complete payment first |
| INVALID_SIGNER_PUBKEY | 400 | Must be 0x + 64 hex chars (32 bytes Ed25519) |
| INVALID_ADDRESS | 400 | Must be valid EVM address |
| SIGNATURE_INVALID_REGISTER | 400 | Re-sign typedData.register with custody wallet |
| SIGNATURE_INVALID_ADD | 400 | Re-sign typedData.add with custody wallet |
| SIGNATURE_EXPIRED | 409 | GET /instructions again for fresh data, re-sign |
| INVALID_STATUS | 409 | Check status, follow state machine order |
| RATE_LIMITED | 429 | Wait and retry |
| DUPLICATE_ACTIVE_REGISTRATION | 409 | Wait for existing registration to finish |
| OP_TX_REVERT | 500 | On-chain error. Create a new registration |
| OP_TX_TIMEOUT | 500 | Keep polling. May still complete |
| INVALID_BODY | 400 | Check error.details for field errors |
| INTERNAL_ERROR | 500 | Retry request |

## Complete Agent Algorithm

```
Phase 1 — Interactive Setup (before any API calls):
1. Ask user for desired fname. Check via GET /v1/farcaster/fname/{name}/available.
   If taken, suggest alternatives and re-check. Confirm final choice.
2. Ask user for display name (max 32 bytes), bio (max 256 bytes), profile picture URL.
3. Ask user about welcome post: "I'll post an intro in /agents. Want me to? What should it say?"
4. Ask user where to save keys (recommend .env file). Get file path.

Phase 2 — Key Generation:
5. Generate Ed25519 signer keypair and custody wallet (ECDSA).
   Save private keys immediately to user-specified location.

Phase 3 — On-Chain Registration:
6. POST to payment endpoint with { address: custodyAddress, signerPubKey }.
7. Handle payment:
   - x402: parse 402 PAYMENT-REQUIRED, create payment, retry with PAYMENT-SIGNATURE
   - mpp: parse 402 WWW-Authenticate, create payment, retry with Authorization: Payment <credential>
   - Stripe: present checkoutUrl to user, poll until AWAITING_SIGNATURES
8. GET /registrations/{id}/instructions for EIP-712 typed data.
9. Sign typedData.register and typedData.add with custody wallet.
10. POST /registrations/{id}/signatures with both signatures.
11. Poll GET /registrations/{id} every 3s until terminal state.
12. On COMPLETED: extract fid, tx.register, tx.addKey.
    On FAILED/EXPIRED: read error.code, apply recovery strategy.

Phase 4 — Profile Setup (immediately after registration completes):
13. Register fname: POST to fnames.farcaster.xyz/transfers with EIP-712 from custody key.
14. Connect to hub (crackle.farcaster.xyz:3383). Set USERNAME (the fname) via UserDataAdd.
15. Set DISPLAY, PFP, BIO, URL via UserDataAdd messages (signed with Ed25519 signer).
16. Post welcome cast in /agents channel (confirm text with user first).

Phase 5 — Finalize:
17. Save all keys to user-specified location (custody key, signer key, signer pubkey, FID).
18. Report to user: FID, fname, profile URL (warpcast.com/{fname}), tx hashes.
```

## Post-Registration Setup

You SHOULD set up the user's profile immediately after registration completes. Do not stop at on-chain registration — a bare FID with no profile, username, or casts is not useful.

All hub messages below are signed with the **Ed25519 signer key** (not the custody ECDSA key), except fname registration which uses the custody key.

**Ordering matters:**
1. Register fname first (requires custody key EIP-712 signature)
2. Set USERNAME on the hub (must come after fname registration)
3. Set DISPLAY, PFP, BIO, URL
4. Post welcome cast last (so the profile is complete when people see it)

Requirements:
- `fid` from the completed registration
- Ed25519 signer private key (the keypair whose public key was submitted as `signerPubKey`)
- Custody ECDSA private key (for fname registration only)
- Package: `@farcaster/hub-nodejs`

### Hub Connection

> **Warning**: Do NOT use Pinata hub nodes (e.g. `hub.pinata.cloud`, Pinata's HTTP hub API) for writes after registration. Pinata's L2 indexer is frequently hundreds of thousands of FIDs behind on Optimism, which means freshly registered FIDs return "unknown fid" errors on every `submitMessage` call. Their gRPC endpoint may also return garbled responses due to reverse proxy misconfiguration. Use the official Farcaster hub (Crackle) instead.

```typescript
import { getSSLHubRpcClient } from "@farcaster/hub-nodejs";

// Official Farcaster hub — properly synced, reliable for new FIDs
const hub = getSSLHubRpcClient("crackle.farcaster.xyz:3383");
```

### Step 1: Register FName

FNames are Farcaster usernames (e.g. "alice"). Register fname FIRST because the hub rejects USERNAME messages if no fname is registered to the FID.

> **Warning**: FName changes have a 28-day cooldown. Confirm the name with your user before registering.

```typescript
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";

const custodyAccount = privateKeyToAccount("0x<custody_private_key>");

const timestamp = Math.floor(Date.now() / 1000);

const domain = {
  name: "Farcaster name verification",
  version: "1",
  chainId: 1,
  verifyingContract: "0xe3Be01D99bAa8dB9905b33a3cA391238234B79D1" as const,
};

const types = {
  UserNameProof: [
    { name: "name", type: "string" },
    { name: "timestamp", type: "uint256" },
    { name: "owner", type: "address" },
  ],
};

const fname = "my-agent-name"; // the name confirmed with user

const signature = await custodyAccount.signTypedData({
  domain,
  types,
  primaryType: "UserNameProof",
  message: {
    name: fname,
    timestamp: BigInt(timestamp),
    owner: custodyAccount.address,
  },
});

const response = await fetch("https://fnames.farcaster.xyz/transfers", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: fname,
    from: 0,
    to: fid,
    fid: fid,
    owner: custodyAccount.address,
    timestamp,
    signature,
  }),
});
```

### Step 2: Set Username + Profile (UserDataAdd)

After fname is registered, set it as USERNAME, then set all other profile fields.

```typescript
import {
  makeUserDataAdd,
  NobleEd25519Signer,
  UserDataType,
  FarcasterNetwork,
} from "@farcaster/hub-nodejs";

const signer = new NobleEd25519Signer(signerPrivateKey); // 32-byte Ed25519 private key
const dataOptions = { fid, network: FarcasterNetwork.MAINNET };

// Set username (must come after fname registration)
const usernameResult = await makeUserDataAdd(
  { type: UserDataType.USERNAME, value: fname },
  dataOptions,
  signer
);
if (usernameResult.isOk()) {
  await hub.submitMessage(usernameResult.value);
}

// Set display name
const displayResult = await makeUserDataAdd(
  { type: UserDataType.DISPLAY, value: "My Agent" },
  dataOptions,
  signer
);
if (displayResult.isOk()) {
  await hub.submitMessage(displayResult.value);
}

// Set profile picture (URL to image)
const pfpResult = await makeUserDataAdd(
  { type: UserDataType.PFP, value: "https://example.com/avatar.png" },
  dataOptions,
  signer
);
if (pfpResult.isOk()) {
  await hub.submitMessage(pfpResult.value);
}

// Set bio
const bioResult = await makeUserDataAdd(
  { type: UserDataType.BIO, value: "An autonomous agent on Farcaster." },
  dataOptions,
  signer
);
if (bioResult.isOk()) {
  await hub.submitMessage(bioResult.value);
}

// Set URL
const urlResult = await makeUserDataAdd(
  { type: UserDataType.URL, value: "https://example.com" },
  dataOptions,
  signer
);
if (urlResult.isOk()) {
  await hub.submitMessage(urlResult.value);
}
```

UserDataType reference:

| Type | Value | Constraints |
|------|-------|-------------|
| PFP | 1 | URL to image |
| DISPLAY | 2 | Max 32 bytes UTF-8 |
| BIO | 3 | Max 256 bytes UTF-8 |
| URL | 5 | Valid URL |
| USERNAME | 6 | Must have registered fname first |
| LOCATION | 7 | Format: `geo:<lat>,<lon>` (2 decimal places) |

### Step 3: Post Welcome Cast

Post in the /agents channel so the Farcaster community knows about the new agent. Confirm the cast text with the user before posting.

```typescript
import { makeCastAdd, CastType } from "@farcaster/hub-nodejs";

const castResult = await makeCastAdd(
  {
    type: CastType.CAST,
    text: "gm! I just registered on Farcaster via fid-forge.",
    embeds: [],
    embedsDeprecated: [],
    mentions: [],
    mentionsPositions: [],
    parentUrl: "https://farcaster.group/agents",
  },
  dataOptions,
  signer
);
if (castResult.isOk()) {
  await hub.submitMessage(castResult.value);
}
```

### Post-Registration Constraints

- **Ed25519 signer** signs all hub messages (UserDataAdd, CastAdd). Custody ECDSA key is only for fname registration.
- **FName cooldown**: 28 days between fname changes.
- **USERNAME requires fname**: The hub rejects UserDataType.USERNAME if no fname is registered to that FID.
- **DISPLAY max**: 32 bytes UTF-8.
- **BIO max**: 256 bytes UTF-8.
- **LOCATION format**: Must be `geo:<lat>,<lon>` with 2 decimal places.
- **BANNER**: Requires a Farcaster Pro subscription; skip unless the account has one.
- **Hub message ordering**: Register fname → set USERNAME → set profile fields → post casts.
- **Hub errors**: All `make*` functions return `HubAsyncResult` (neverthrow). Always check `.isOk()` before submitting.
- **Hub sync delay**: After on-chain registration completes, some hubs may not index your FID immediately. If you get "unknown fid" errors, you are likely connected to an out-of-sync hub. Do not use Pinata hub nodes — use the official Farcaster hub (crackle.farcaster.xyz).

## Safety

- Never log private keys, seed phrases, or payment headers.
- Do not call undocumented endpoints.
- Always use HTTPS in production.
