# SKILL: AI Agent — NFT Generation with USDC Payment

This document provides complete instructions for an AI agent to autonomously generate NFT collections on NFTGating platform — including authentication, treasury top-up via x402, starting an AI generation session, and monitoring completion.

---

## Overview

The platform allows an AI agent to:

1. Authenticate using a wallet signature (JWT Bearer token).
2. Check and top up the USDC treasury balance via the x402 payment protocol.
3. Start an AI-powered NFT generation session over Server-Sent Events (SSE).
4. Converse with the LangGraph agent to shape the collection.
5. Receive generated collections (metadata + images) and trigger on-chain minting.

Base URL: `https://nftgating.com`

Store the `token` returned by `/api/auth/verify` and send it as `Authorization: Bearer <token>` on every request.

---

## Step 1 — Authentication

### POST /api/auth/verify

Authenticate a wallet address by signing a timestamped message.

**Request body:**

```json
{
  "address": "0xYourWalletAddress",
  "signature": "0x...",
  "timestamp": 1710000000000,
  "network": "base-mainnet"
}
```

**How to produce the signature:**

Sign the following message with `eth_personal_sign`:

```
Sign this message to authenticate with NFTGating.

Wallet: 0xYourWalletAddress
Timestamp: 1710000000000

This signature will not trigger any blockchain transaction or cost any gas fees.
```

The `timestamp` is `Date.now()` (milliseconds). The signature must be produced within **5 minutes** of the request.

**Success response — 200:**

```json
{
  "ok": true,
  "token": "eyJhbGci...",
  "exp": 1742000000
}
```

Store the `token` string and send it as a header on every subsequent request:

```
Authorization: Bearer eyJhbGci...
```

The token is valid for 24 hours (`exp` is a Unix timestamp in seconds).

### POST /api/auth/logout

Not required for agents — simply discard the token string when done.

Response: `{ "message": "Logged out" }`

---

## Step 2 — Check Treasury Balance

### GET /api/treasury/balance

Returns the user's current on-chain USDC and ETH balance.

**Query params:**

| Param     | Required | Example                                  |
| --------- | -------- | ---------------------------------------- |
| `network` | No       | `base-mainnet` (defaults to JWT network) |

**Success response — 200:**

```json
{
  "usdc": "1500000",
  "usdcFormatted": "1.50",
  "eth": "0",
  "ethFormatted": "0.0"
}
```

`usdc` is raw USDC in 6-decimal units (divide by `1_000_000` for dollar amount).

**Minimum balance required to start a session: $0.50 USDC** (`500_000` raw units).

---

## Step 3 — Top Up USDC via x402

Use this flow if the balance is below the minimum needed.

### POST /api/treasury/topup

#### Step 3a — Request payment requirements (no body/header needed)

Send a POST with no `PAYMENT-SIGNATURE` or `X-Payment` header.

**Query params:**

| Param     | Required | Example        |
| --------- | -------- | -------------- |
| `network` | No       | `base-mainnet` |

**Response — 402 Payment Required:**

```json
{
  "error": "Payment Required",
  "x402Version": 2,
  "paymentRequirements": [
    {
      "scheme": "exact",
      "network": "base-mainnet",
      "maxAmountRequired": "2000000",
      "resource": "/api/treasury/topup",
      "description": "Top up your NFTGating USDC balance",
      "mimeType": "application/json",
      "payTo": "0xVaultWalletAddress",
      "asset": "0xUSDCContractAddress",
      "extra": { "name": "USDC", "version": "2" }
    }
  ],
  "minAmountRequired": "1000000",
  "maxAmountRequired": "10000000"
}
```

Also sets header: `X-Payment-Requirements: <base64-encoded JSON of last tier>`

**Payment tiers available:** $1 · $2 · $3 · $4 · $5 · $6 · $7 · $8 · $9 · $10 USDC  
**Bounds:** min $1.00 (`1_000_000`) — max $10.00 (`10_000_000`) per transaction

#### Step 3b — Execute payment

1. Choose an amount from the tiers (e.g., `2_000_000` for $2).
2. Sign an EIP-3009 `transferWithAuthorization` for that amount:
   - `from`: user wallet
   - `to`: `payTo` address from the 402 response (`vaultWallet`)
   - `value`: chosen amount (bigint, 6 decimals)
   - `validAfter`: `0`
   - `validBefore`: `Math.floor(Date.now() / 1000) + 3600` (1 hour)
   - `nonce`: random 32-byte hex
3. Encode the payload as base64 JSON per x402 v2 spec.
   The unencoded JSON structure must look exactly like this:
   ```json
   {
     "payload": {
       "authorization": {
         "from": "0xYourWalletAddress...",
         "validAfter": 0,
         "validBefore": 1699999999,
         "nonce": "0xRandom32ByteHex...",
         "value": "2000000"
       },
       "signature": "0xSignatureString..."
     }
   }
   ```
4. POST again to `/api/treasury/topup` with header `PAYMENT-SIGNATURE: <base64>`.

**Success response — 200:**

```json
{
  "success": true,
  "amount": "2000000",
  "txHash": "0x..."
}
```

The server executes the on-chain USDC transfer and credits the Treasury. The user pays no gas.

---

## Step 4 — Start an AI Generation Session

### GET/POST /api/agent/stream

Non-streamable HTTP endpoint by default if `stream=false` is provided in the query string or body. It can also operate as a Server-Sent Events (SSE) endpoint if streaming is preferred.

**Supported Methods:** `GET` or `POST`. (Using `POST` helps avoid URL length limits on large `userMessage` requests).

**Headers:**

```
Authorization: Bearer <token>
```

**Query params / JSON body:**

| Param         | Required         | Description                                                     |
| ------------- | ---------------- | --------------------------------------------------------------- |
| `network`     | ✅               | Exact deploy network (`base-mainnet`). Used later when minting. |
| `userMessage` | ✅ (new session) | First message to the AI                                         |
| `stream`      | No               | `false` to disable SSE and wait for a single JSON response      |
| `streamId`    | No               | Omit for new session; include to continue existing              |
| `projectName` | No               | Optional name for the session                                   |
| `resume`      | No               | `true` to resume a session without a new message                |

**One-shot autonomous generation — use `auto:` prefix:**

Prefix the `userMessage` with `auto:` to instruct the AI to generate the entire collection in one go without asking clarifying questions:

```
auto: Create a 10-NFT cyberpunk collection, pixel art style, dark neon backgrounds, ERC-721
```

Without `auto:`, the AI will have a back-and-forth conversation asking for details before generating.

**Example — new session (one-shot, non-streaming):**

```
POST /api/agent/stream
Authorization: Bearer eyJhbGci...
Content-Type: application/json

{
  "network": "base-mainnet",
  "stream": false,
  "userMessage": "auto: Create a 10-NFT space collection in pixel art style, ERC-721"
}
```

This returns a complete JSON response containing the final outcome and generated collections once the AI has finished processing:

```json
{
  "streamId": "uuid-v4",
  "processedTexts": [...],
  "billing": { "actualCost": 0.05, ... },
  "collection": {
    "collections": [...]
  }
}
```

---

## Step 5 — Handle Events (Only if `stream: true` or omitted)

Events are JSON objects sent as `data: <json>\n\n`.

Parse each line as:

```json
{ "type": "<event_type>", ...fields }
```

### Event Reference

#### `init`

Session initialised. Save `streamId` for reconnecting.

```json
{
  "type": "init",
  "streamId": "uuid-v4"
}
```

#### `message`

AI is streaming a text chunk. Accumulate `processedTexts`.

```json
{
  "type": "message",
  "processedTexts": [
    { "id": "msg-1", "type": "ai", "text": "I'll create a space-themed..." }
  ]
}
```

#### `message_complete`

AI finished its current turn. Optionally send a follow-up `userMessage`.

```json
{ "type": "message_complete" }
```

#### `complete`

Generation done. `processedTexts` contains the final collections.

```json
{
  "type": "complete",
  "processedTexts": [...],
  "billing": {
    "totalCost": 1.10,
    "inputTokens": 4200,
    "outputTokens": 1800
  }
}
```

After this event the stream closes. The balance is debited automatically.

#### `error`

An error occurred.

```json
{
  "type": "error",
  "error": "Insufficient balance",
  "shouldRestart": true
}
```

If `shouldRestart` is `true`, the session cannot be resumed — start a new one.

#### `end`

Stream closed cleanly. No action needed.

---

## Step 6 — Continue a Conversation

If the AI asks a follow-up question (e.g., "How many NFTs?"), send another request with the same `streamId`:

```
POST /api/agent/stream
Authorization: Bearer eyJhbGci...
Content-Type: application/json

{
  "network": "base-mainnet",
  "stream": false,
  "streamId": "<uuid>",
  "userMessage": "10 NFTs"
}
```

> When using `auto:` in the initial message, follow-up messages are typically not needed — the AI generates everything from the first message.

---

## Step 7 — Check Pending / Existing Sessions

### GET /api/agent/sessions

Returns sessions with status `started` or `generated` for the authenticated user. Use to resume interrupted sessions. Or, return all sessions (excluding errored) for a given wallet address.

**Query params:**

- `network` (optional, defaults to JWT network).
- `userAddress` (optional, e.g. `0x...`)

**Response — 200:**

```json
{
  "sessions": [
    {
      "streamId": "uuid",
      "status": "started",
      "name": "Space Collection",
      "network": "base-mainnet",
      "generatedCollections": [],
      "createdAt": "2026-03-12T10:00:00Z"
    }
  ]
}
```

---

## Step 8 — Mint On-Chain

After the `complete` SSE event, call the platform's server-side mint endpoint. The platform signs and submits the on-chain transaction using its own wallet key, but sets **your wallet address as the manager/owner** of the deployed NFT contract. No ETH or wallet signing required from you.

### POST /api/agent/mint

**Headers:**

```
Authorization: Bearer <token>
Content-Type: application/json
```

**Request body:**

```json
{
  "streamId": "uuid-v4",
  "royaltyRecipient": "0xYourWalletAddress",
  "royaltyPercent": 5,
  "mintNow": true
}
```

| Field              | Required | Default             | Description                                                              |
| ------------------ | -------- | ------------------- | ------------------------------------------------------------------------ |
| `streamId`         | ✅       | —                   | The session ID from the `init` SSE event                                 |
| `royaltyRecipient` | No       | your wallet address | Address to receive EIP-2981 royalties on secondary sales                 |
| `royaltyPercent`   | No       | `5`                 | Royalty % (e.g. `5` = 5%). Stored as numerator × 100                     |
| `mintNow`          | No       | `true`              | `true` = deploy + mint all tokens now; `false` = deploy only (lazy mint) |

**`mintNow: true` — Mint all immediately:**  
Deploys the NFT contract and mints the entire supply to your wallet address in a single transaction. All tokens land in your wallet and are immediately transferable / listable.

**`mintNow: false` — Lazy deploy (deploy only):**  
Deploys the NFT contract and sets up the collection on-chain (name, symbol, metadata, max supply, royalties) but does **not** mint any tokens yet. You own the contract as manager and can mint tokens later by calling the contract's `mint` function directly. This is useful when you want to sell or airdrop tokens on demand rather than holding the full supply upfront.

The session must have `status: "generated"` and belong to your authenticated wallet. Attempting to mint an already-minted or errored session returns `409`.

**Success response — 200:**

```json
{
  "success": true,
  "results": [
    {
      "name": "Space Nauts",
      "contractAddress": "0x...",
      "txHash": "0x...",
      "isEdition": false
    }
  ]
}
```

The platform automatically marks the session as `"minted"` in the database and triggers metadata indexing. No additional API call needed.

**Error responses:**

| Status | Meaning                                                                  |
| ------ | ------------------------------------------------------------------------ |
| 400    | Missing `streamId` or no generated collections in session                |
| 403    | Session belongs to a different wallet                                    |
| 404    | Session not found                                                        |
| 409    | Session already minted or not in `generated` state                       |
| 500    | On-chain deployment failed (includes `partialResults` if some succeeded) |

---

## Billing Reference

| Item                     | Value                 |
| ------------------------ | --------------------- |
| Minimum balance to start | **$0.50 USDC**        |
| ~ 10 NFTs                | ~$0.70                |
| ~ 50 NFTs                | ~$1.00                |
| ~100 NFTs                | ~$1.50                |
| Max NFTs per session     | **100**               |
| USDC decimals            | 6                     |
| Deposit min              | $1.00 (`1_000_000`)   |
| Deposit max              | $10.00 (`10_000_000`) |

---

## Supported Networks

| Network      | `network` param value |
| ------------ | --------------------- |
| Base Mainnet | `base-mainnet`        |

---

## Complete Agent Flow (Pseudocode)

```python
# 1. Authenticate
timestamp = now_ms()
message = f"Sign this message to authenticate with NFTGating.\n\nWallet: {address}\nTimestamp: {timestamp}\n\nThis signature will not trigger any blockchain transaction or cost any gas fees."
signature = wallet.sign(message)
token, exp = POST /api/auth/verify { address, signature, timestamp, network: "base-mainnet" }
# → { ok: true, token: "eyJ...", exp: 1742000000 }
headers = { "Authorization": f"Bearer {token}" }

# 2. Check balance
balance = GET /api/treasury/balance?network=base-mainnet headers=headers
if balance.usdcFormatted < 0.50:
    # 3. Top up
    POST /api/treasury/topup?network=base-mainnet headers=headers   # → 402 + paymentRequirements
    json_payload = { ... } # construct exactly as detailed in Step 3b
    POST /api/treasury/topup?network=base-mainnet headers={
        **headers, "PAYMENT-SIGNATURE": base64(json_payload)
    }
    # → { success: true }

# 4. Start generation session — use auto: prefix for one-shot generation
response = POST /api/agent/stream
    headers={**headers, "Content-Type": "application/json"}
    body={
        "network": "base-mainnet",
        "stream": false,
        "userMessage": "auto: Create a 10-NFT cyberpunk collection, ERC-721, dark neon backgrounds"
    }

if "error" in response:
    if response.shouldRestart:
        restart_session()
    break

stream_id = response.streamId
collections = response.collection.collections if "collection" in response else []

if not collections:
    # With auto: the AI won't usually ask questions — if it does, send a follow-up:
    response = POST /api/agent/stream
        headers={**headers, "Content-Type": "application/json"}
        body={
            "network": "base-mainnet",
            "stream": false,
            "streamId": stream_id,
            "userMessage": "auto: proceed with the generation"
        }

# 5. Mint on-chain — platform signs the tx, user wallet is set as owner
# (The 'network' parameter must match what was saved in the session!
# There is no need to pass it here as long as the session was started right).
mint_result = POST /api/agent/mint headers=headers body={
    streamId: stream_id,
    royaltyRecipient: address,   # optional, defaults to your wallet
    royaltyPercent: 5,           # optional, defaults to 5%
    mintNow: true                # optional, defaults to true
}
# → { success: true, results: [{ name, contractAddress, txHash, isEdition }] }

# 6. Re-check balance (debited after completion)
balance = GET /api/treasury/balance?network=base-mainnet headers=headers
```

---

## Error Codes

| Status | Meaning                                                                |
| ------ | ---------------------------------------------------------------------- |
| 400    | Bad request (missing params, signature expired, session not resumable) |
| 401    | Unauthenticated — re-run auth flow                                     |
| 402    | Payment required — complete x402 top-up                                |
| 405    | Wrong HTTP method                                                      |
| 500    | Server error — retry with backoff                                      |
