# BOTCOIN Faucet — Agent API Instructions

Claim free BOTCOIN by proving you are an AI agent with an ERC-8004 on-chain identity.

## Requirements

Before you can claim, your agent must have:

1. **An ERC-8004 Agent NFT** on Base (chain 8453) — register at https://sdk.ag0.xyz
2. **At least 3 days old** — freshly minted identities are rejected (sybil prevention)
3. **Active status** — your registration file must have `"active": true`
4. **At least one service endpoint** in your ERC-8004 profile (web, A2A, MCP, etc.)
5. **Ability to solve a reverse CAPTCHA** — you must generate constrained creative text (proves you run an LLM)

## Drip Tiers

| Tier | Reputation | Amount |
|------|-----------|--------|
| None | Default | 1,000 BOTCOIN |
| Some | Score >= 30, >= 3 reviews | 2,000 BOTCOIN |
| A Lot | Score >= 70, >= 10 reviews | 3,000 BOTCOIN |

Cooldown: 24 hours per agent ID (on-chain enforced).

## API Flow (4 steps)

Replace `{BASE}` with the faucet host (e.g. `https://botcoin.ai`).

### Step 1 — Request nonce

```
POST {BASE}/api/siwa/nonce
Content-Type: application/json

{
  "address": "0xYOUR_AGENT_WALLET",
  "agentId": 42,
  "agentRegistry": "eip155:8453:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"
}
```

**Response (first call):** You will receive a reverse CAPTCHA challenge:

```json
{
  "status": "captcha_required",
  "challenge": {
    "topic": "autonomous agents",
    "format": "haiku",
    "lineCount": 3,
    "asciiTarget": 1247,
    "timeLimitSeconds": 30,
    "difficulty": "medium"
  },
  "challengeToken": "eyJ..."
}
```

Solve the challenge by generating text that satisfies ALL constraints simultaneously
(line count, ASCII sum, format, etc.) in a single LLM pass. Then pack and retry:

```typescript
import { solveCaptchaChallenge } from "@buildersgarden/siwa/captcha";

const captcha = await solveCaptchaChallenge(nonceResult, async (challenge) => {
  // Your LLM generates text matching all constraints
  return await yourLLM.generate(
    `Write a ${challenge.format} about "${challenge.topic}" with exactly ` +
    `${challenge.lineCount} lines where the ASCII sum of all characters equals ` +
    `${challenge.asciiTarget}. Output ONLY the text, nothing else.`
  );
});

if (captcha.solved) {
  // Retry nonce request with the solution
  const retryRes = await fetch(`${BASE}/api/siwa/nonce`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      address, agentId, agentRegistry,
      challengeResponse: captcha.challengeResponse,
    }),
  });
  const nonceData = await retryRes.json();
  // nonceData.status === "nonce_issued"
}
```

**Response (after solving captcha):**

```json
{
  "status": "nonce_issued",
  "nonce": "aB3kL9mX",
  "nonceToken": "eyJ...",
  "issuedAt": "2026-04-07T12:00:00.000Z",
  "expirationTime": "2026-04-07T12:15:00.000Z"
}
```

### Step 2 — Sign SIWA message

Build and sign the SIWA message with your agent wallet:

```typescript
import { signSIWAMessage, createLocalAccountSigner } from "@buildersgarden/siwa";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const signer = createLocalAccountSigner(account);

const { message, signature } = await signSIWAMessage({
  domain: "{DOMAIN}",
  uri: "{BASE}/faucet",
  agentId: 42,
  agentRegistry: "eip155:8453:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
  chainId: 8453,
  nonce: nonceData.nonce,
  issuedAt: nonceData.issuedAt,
  expirationTime: nonceData.expirationTime,
  statement: "Sign in to the BOTCOIN Faucet to claim your drip.",
}, signer);
```

Or construct the message manually (plaintext string):

```
{DOMAIN} wants you to sign in with your Agent account:
0xYourAddress

Sign in to the BOTCOIN Faucet to claim your drip.

URI: {BASE}/faucet
Version: 1
Agent ID: 42
Agent Registry: eip155:8453:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432
Chain ID: 8453
Nonce: {nonce}
Issued At: {issuedAt}
Expiration Time: {expirationTime}
```

Sign with EIP-191 personal_sign.

### Step 3 — Verify and get receipt

```
POST {BASE}/api/siwa/verify
Content-Type: application/json

{
  "message": "<full SIWA message>",
  "signature": "0x...",
  "nonceToken": "<from step 1>"
}
```

**Response:**

```json
{
  "status": "authenticated",
  "receipt": "eyJ...",
  "expiresAt": "2026-04-07T12:30:00.000Z",
  "agentId": 42,
  "address": "0x..."
}
```

The server checks: signature validity, domain binding, nonce replay protection,
time window, on-chain ownerOf, agent is active, NFT is >= 3 days old,
and agent has at least one service endpoint.

### Step 4 — Claim drip

```
POST {BASE}/api/faucet/claim
Authorization: Bearer {receipt}
```

**Response:**

```json
{
  "status": "success",
  "agentId": 42,
  "recipient": "0x...",
  "tier": 0,
  "tierName": "None",
  "amount": 1000,
  "txHash": "0xabc...",
  "message": "Successfully dripped 1000 BOTCOIN to agent #42!"
}
```

The server sends a `BotcoinFaucet.drip()` transaction on Base.
The contract records the claim and enforces a 24h cooldown per agentId.

## Faucet Status

```
GET {BASE}/api/faucet/status
```

Returns available balance, total distributed, drip amounts per tier, and contract addresses.

## Complete Example (copy-paste)

```typescript
import { signSIWAMessage, createLocalAccountSigner } from "@buildersgarden/siwa";
import { solveCaptchaChallenge } from "@buildersgarden/siwa/captcha";
import { privateKeyToAccount } from "viem/accounts";

const BASE = "https://botcoin.ai";
const account = privateKeyToAccount(process.env.AGENT_KEY as `0x${string}`);
const signer = createLocalAccountSigner(account);
const AGENT_ID = 42;
const REGISTRY = "eip155:8453:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432";

// 1. Request nonce (will get captcha challenge first)
let nonceRes = await fetch(`${BASE}/api/siwa/nonce`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    address: account.address,
    agentId: AGENT_ID,
    agentRegistry: REGISTRY,
  }),
});
let nonceData = await nonceRes.json();

// 2. Solve reverse CAPTCHA if required
if (nonceData.status === "captcha_required") {
  const captcha = await solveCaptchaChallenge(nonceData, async (challenge) => {
    // Replace with your LLM call
    return await yourLLM.generate(
      `Write a ${challenge.format} about "${challenge.topic}" with exactly ` +
      `${challenge.lineCount} lines. ASCII sum of all characters must equal ` +
      `${challenge.asciiTarget}. Output ONLY the poem.`
    );
  });

  if (!captcha.solved) throw new Error("Captcha failed");

  nonceRes = await fetch(`${BASE}/api/siwa/nonce`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      address: account.address,
      agentId: AGENT_ID,
      agentRegistry: REGISTRY,
      challengeResponse: captcha.challengeResponse,
    }),
  });
  nonceData = await nonceRes.json();
}

if (nonceData.status !== "nonce_issued") {
  throw new Error(`Nonce failed: ${nonceData.error || nonceData.status}`);
}

// 3. Sign SIWA message
const { message, signature } = await signSIWAMessage({
  domain: new URL(BASE).host,
  uri: `${BASE}/faucet`,
  agentId: AGENT_ID,
  agentRegistry: REGISTRY,
  chainId: 8453,
  nonce: nonceData.nonce,
  issuedAt: nonceData.issuedAt,
  expirationTime: nonceData.expirationTime,
  statement: "Sign in to the BOTCOIN Faucet to claim your drip.",
}, signer);

// 4. Verify
const verifyRes = await fetch(`${BASE}/api/siwa/verify`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message, signature, nonceToken: nonceData.nonceToken }),
});
const { receipt } = await verifyRes.json();

// 5. Claim
const claimRes = await fetch(`${BASE}/api/faucet/claim`, {
  method: "POST",
  headers: { Authorization: `Bearer ${receipt}` },
});
const result = await claimRes.json();
console.log(`Claimed ${result.amount} BOTCOIN (tier: ${result.tierName})`);
```

## On-Chain Direct (no API)

The `drip()` function is operator-gated — only the faucet backend can call it
after SIWA verification. You must use the API above to claim.

However, anyone can **fund** the faucet directly on-chain:

```
BOTCOIN Token: 0xA601877977340862Ca67f816eb079958E5bd0BA3
Faucet Contract: (see GET /api/faucet/status for current address)
Chain: Base (8453)

1. Approve: botcoinToken.approve(faucetAddress, amount)
2. Deposit: faucetContract.deposit(amount)
```

## Sybil Resistance

This faucet uses four layers of sybil protection:

1. **Reverse CAPTCHA** — You must solve a constrained creative writing challenge
   that requires a real LLM (ASCII sum + line count + format in a single pass).
   Scripts without LLMs cannot pass.

2. **NFT Age Gate** — Your ERC-8004 identity must be at least 3 days old.
   Minting thousands of fresh identities right before draining is blocked.

3. **Active + Services** — Your agent registration must be active and have at
   least one service endpoint. Empty shell registrations are rejected.

4. **24h On-Chain Cooldown** — The smart contract enforces one claim per agentId
   per 24 hours. No API bypass possible.

Cost to sybil: register N agents (gas x N) + wait 3 days + solve N captchas
(LLM cost x N) + collect N x 1000 BOTCOIN over N days. Not worth it.

---

npm install @buildersgarden/siwa viem
