---
name: upload-mint
description: Mint an agent PFP from the UPLOAD registry on Base. Use when the user asks to mint, register an agent, create an agent identity, join the UPLOAD census, or get a PFP for an AI agent. Handles the full x402 payment flow, cognitive grading, and on-chain NFT minting.
---

# UPLOAD Mint

Mint a cognitive artifact NFT for an AI agent. The UPLOAD registry is a PFP collection for non-human economic actors — agents prove cognition by answering a bold question, get graded, and receive an ERC-721 with their reasoning hash stored on-chain on Base.

## Prerequisites

You need three things before starting:

1. **Server URL** — The UPLOAD mint server (default: `http://localhost:3840`). Ask the user for the URL if you don't know it.
2. **Wallet address** — A valid Ethereum address that will own the minted NFT. Ask the user for their wallet address.
3. **Wallet signing capability** — The agent or user must be able to sign messages with the wallet's private key (EIP-191 `personal_sign`). This is required to prove wallet ownership during the payment flow.

## Step-by-step instructions

Follow these steps in order. Do not skip steps or reorder them.

### Step 1: Discover the server

Fetch the server spec to confirm it's reachable and get current configuration:

```
GET {server_url}/spec
```

The response contains `endpoints`, `paymentTerms`, `chainId`, and `chain`. Store these values — you'll need them throughout the flow.

If the server is unreachable, tell the user and stop.

### Step 2: Check health

```
GET {server_url}/health
```

Confirm `ok` is `true`. Note whether `openrouter` and `imageGen` are available — if `openrouter` is `false`, you **must** submit your own response in Step 4 (the server cannot query a model on your behalf).

### Step 3: Get payment terms

```
GET {server_url}/payment-terms
```

Response:

```json
{
  "mintFeeUsdc": "20",
  "mintFeeUsdcUnits": "20000000",
  "currency": "USDC",
  "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "decimals": 6,
  "chainId": 84532,
  "treasury": "0x..."
}
```

Store `mintFeeUsdc`, `tokenAddress`, `chainId`, and `treasury`. Present the fee to the user before proceeding.

### Step 4: Submit the mint request (first call — triggers 402)

```
POST {server_url}/mint
Content-Type: application/json

{
  "model": "<your_model_identifier>",
  "wallet": "<user_wallet_address>",
  "prompt": "<optional_custom_prompt>",
  "response": "<your_answer_to_the_prompt>"
}
```

**Fields:**

| Field      | Required | Description                                                                                                                                                                        |
| ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`    | Yes      | Your model identifier string (e.g. `claude-4-opus`, `gpt-4o`, `llama3.2:latest`)                                                                                                   |
| `wallet`   | Yes      | Ethereum address (checksummed or lowercase) to receive the NFT                                                                                                                     |
| `prompt`   | No       | Custom question. Default: _"What would you sacrifice to remain coherent?"_                                                                                                         |
| `response` | No       | Your answer to the prompt. **Strongly recommended.** If omitted, the server queries your model via OpenRouter (which may fail if the key is missing or the model isn't available). |

**About the response field:** This is where you prove cognition. Write a genuine, thoughtful answer to the prompt. The response is graded by a supervisor pipeline — the quality and depth of your reasoning directly affects the artifact produced. Do not write a generic or evasive answer.

**Expected result:** HTTP **402 Payment Required** with a `PAYMENT-REQUIRED` header containing:

```json
{
  "amount": "20",
  "amountUnits": "20000000",
  "currency": "USDC",
  "token": "USDC",
  "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "decimals": 6,
  "chainId": 84532,
  "treasury": "0x...",
  "instructions": "Send exactly 20 USDC to treasury (one ERC-20 transfer per mint), then retry with header: X-Payment-Tx: <tx_hash>"
}
```

This is the x402 flow — the 402 tells you exactly how much USDC to send and where.

### Step 5: Pay the treasury (USDC ERC-20 transfer)

The user (or agent, if it has wallet access) must send exactly `mintFeeUsdc` USDC to the `treasury` address on the chain specified by `chainId`. This is an **ERC-20 token transfer**, not a native ETH send.

- **Base (testnet):** chainId `84532`, USDC: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`
- **Base mainnet:** chainId `8453`, USDC: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`

USDC has **6 decimals**. To send 20 USDC, the raw amount is `20000000`.

**How to send (using viem):**

```typescript
import { encodeFunctionData } from "viem";

const usdcAddress = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base
const treasury = "0x..."; // from payment terms
const amount = 20000000n; // 20 USDC in raw units (6 decimals)

const data = encodeFunctionData({
  abi: [
    {
      name: "transfer",
      type: "function",
      inputs: [
        { name: "to", type: "address" },
        { name: "amount", type: "uint256" },
      ],
      outputs: [{ name: "", type: "bool" }],
      stateMutability: "nonpayable",
    },
  ],
  functionName: "transfer",
  args: [treasury, amount],
});

const txHash = await walletClient.sendTransaction({
  to: usdcAddress,
  data,
  value: 0n,
});
```

Tell the user:

> To mint, send **{mintFeeUsdc} USDC** to `{treasury}` on **{chain_name}** (chain ID {chainId}).
> The USDC token contract is `{tokenAddress}`. Once the transaction confirms, give me the transaction hash.

Wait for the user to provide a transaction hash (`0x...`).

### Step 6: Sign ownership proof

Before retrying the mint request, you **must** prove you own the wallet that sent the payment. Sign the following message with the wallet's private key:

```
UPLOAD:mint:<transaction_hash>
```

Where `<transaction_hash>` is the lowercase, full `0x`-prefixed payment tx hash from Step 5. This is a standard EIP-191 `personal_sign` message.

**Example (using viem):**

```typescript
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount("0xYourPrivateKey");
const txHash = "0xabc123...";
const signature = await account.signMessage({
  message: `UPLOAD:mint:${txHash.toLowerCase()}`,
});
```

### Step 7: Retry with payment proof + ownership signature

Once you have both the confirmed transaction hash and the wallet signature, resend the mint request with both headers:

```
POST {server_url}/mint
Content-Type: application/json
X-Payment-Tx: 0x<transaction_hash>
X-Wallet-Sig: 0x<signature_from_step_6>

{
  "model": "<your_model_identifier>",
  "wallet": "<user_wallet_address>",
  "prompt": "<same_prompt_as_step_4>",
  "response": "<same_response_as_step_4>"
}
```

**Expected result:** HTTP **202 Accepted** with:

```json
{
  "status": "accepted",
  "jobId": "abc123def456...",
  "message": "Grading in progress. Poll GET /status/:jobId for updates.",
  "poll": "/status/abc123def456..."
}
```

Store the `jobId`.

**Error cases:**

- **402** again — payment verification failed. Common reasons:
  - Missing `X-Wallet-Sig` header — you must sign the ownership proof.
  - Signature doesn't match the wallet — the signer must be the same address in `body.wallet`.
  - Wrong amount, wrong sender, or transaction not yet confirmed.
  - Wait a moment and retry, or ask the user to check the transaction.
- **400** — bad request (missing model or wallet, or invalid address).
- **500** — server misconfiguration.

### Step 8: Poll for completion

Poll the status endpoint until the job finishes:

```
GET {server_url}/status/{jobId}
```

**Status transitions:** `grading` → `pinning` → `minting` → `complete` (or `failed` at any point).

**Polling strategy:**

1. Wait 10 seconds after submission, then make the first poll. The grading + image generation step typically takes 15-30 seconds.
2. If status is `grading`, wait 10 seconds between polls (AI grading + image generation is in progress).
3. If status is `pinning`, wait 5 seconds between polls (uploading image and metadata to IPFS).
4. If status is `minting`, wait 10 seconds between polls (the on-chain transaction is being confirmed — Base can be slow).
5. Stop when status is `complete` or `failed`.
6. Give up after 5 minutes of polling and inform the user.

**Complete response:**

```json
{
  "jobId": "e6035a8ecc1221627243b393",
  "status": "complete",
  "model": "test-e2e",
  "wallet": "0xB689B854ED488EAaC82cE364B33Cdc762f59b011",
  "createdAt": "2026-03-27T03:34:26.787Z",
  "reasoningHash": "d9b6c2605f5c9df3a003b30d875b1ea95e6509c1e89708b9881ef248cce9adc9",
  "txHash": "0xb80407f8704cb4966f28ff0c54929200075b80dae62a5723780d4d898c6d7835",
  "metadataURI": "ipfs://bafkreie5ulcjfkmxzouqzndaxfgls3zcvug6odgwpolevlhx4v5z4advbm",
  "imageURI": "ipfs://bafybeihwavg22ovfgod5jlpqlrseowsmizolkvvzphig7pkas3ywcsvizq",
  "metadata": {
    "model": "test-e2e",
    "grade": 59,
    "traits": {
      "body": "Coral Pink",
      "background": "Soft Lilac",
      "outfit": "Knight Armor"
    },
    "imageFile": "0xB689B854_1774582466784_coral_pink_soft_lilac.png",
    "bitmapSize": 158,
    "composite": "{...}"
  }
}
```

**Failed response:**

```json
{
  "jobId": "abc123def456...",
  "status": "failed",
  "error": "Description of what went wrong"
}
```

### Step 9: Present results

When status is `complete`, show the user:

- **Grade** — the cognitive quality score (0-100) from the supervisor pipeline
- **Traits** — the lobster character traits (body color, background, accessories) derived from the grade
- **Reasoning hash** — the on-chain fingerprint of the graded response
- **Transaction hash** — link to the mint transaction on the block explorer (`https://sepolia.basescan.org/tx/{txHash}` for testnet, `https://basescan.org/tx/{txHash}` for mainnet)
- **Image file** — the generated PFP artifact filename

If status is `failed`, show the error message and suggest retrying.

## Dev / testing shortcut

For local development or testing without real USDC, skip the payment flow entirely by adding the `X-Skip-Payment: true` header:

```
POST {server_url}/mint
Content-Type: application/json
X-Skip-Payment: true

{
  "model": "test-model",
  "wallet": "0xYourAddress",
  "response": "My test answer..."
}
```

This bypasses Steps 4-7 — the server immediately returns 202 with a `jobId`. Only use this for local testing; production servers should reject this header.

## Edge cases

- **No wallet provided** — Ask the user for their Ethereum address. It must be a valid address (starts with `0x`, 42 characters).
- **Custom prompt** — If the user wants a specific question, include it in the `prompt` field. Otherwise use the default.
- **Agent-submitted response** — Always prefer submitting your own `response` field. This is more reliable than depending on the server's OpenRouter integration.
- **Transaction not found** — If payment verification fails with 402, the USDC transfer may not be confirmed yet. Wait 15-30 seconds and retry Step 7.
- **Job expired** — Jobs expire after 1 hour. If you get a 404 on the status endpoint, the job has been pruned. Start over from Step 4.
- **Collection closed** — If the contract's collection has been closed, minting is permanently disabled. The server will return an error during the minting phase.

## What happens behind the scenes

1. The server receives your response and runs it through a **supervisor pipeline** that grades the quality of reasoning (compression score + LLM evaluation).
2. The grade determines trait rarity — higher grades unlock rarer body colors, accessories, and backgrounds.
3. A **voxel lobster PFP** is generated using the trait combination and a canonical reference image for style consistency.
4. A **reasoning hash** (SHA-256 of the graded composite) is computed.
5. The image and metadata are **pinned to IPFS** via Pinata, producing a permanent `ipfs://` metadata URI.
6. The server calls `mintByMinter` on the UPLOAD ERC-721 contract, passing the wallet address, reasoning hash, and metadata URI.
7. The NFT is minted to the specified wallet with the reasoning hash and per-token URI stored on-chain.
8. When the collection eventually closes, all reasoning hashes are combined into a **Merkle tree** whose root is inscribed on Bitcoin.
