# Sideload Agent API

Generate **avatars and Objects for 2 USDC each** using the same x402 payment flow on Base (chain ID 8453).

Base URL: `https://sideload.gg`. No account or API key required. The shared allowance is **10 generations per 30 minutes per wallet**, across both asset kinds. App credits are separate.

## Quick start

Send JSON to `POST /api/agent/generate` using an x402-compatible client, such as thirdweb's `wrapFetchWithPayment`. It reads the unpaid 402 offer and supplies `PAYMENT-SIGNATURE` (v2) or `X-PAYMENT` (v1). Raw curl without a signed payment returns discovery; placeholders are not payment tokens.

Text Object request:

```json
{"assetType":"object","type":"text","prompt":"A weathered wooden treasure chest with brass fittings"}
```

Single-image Object request (replace with your public image URL):

```json
{"assetType":"object","type":"image","imageUrl":"https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/BoxTextured/screenshot/screenshot.png"}
```

The example image is the public Khronos BoxTextured sample screenshot. It is an input reference, not evidence of a paid production test.

Avatar requests stay compatible. Omit `assetType` or set it to `"avatar"`:

```json
{"type":"text","prompt":"A cyberpunk samurai with glowing red armor"}
```

## Generate

`POST /api/agent/generate`

| Field | Meaning |
|---|---|
| `assetType` | `"avatar"` (default) or `"object"`; both cost 2 USDC |
| `type` | Required `"text"` or `"image"`; this is the input mode, not asset kind |
| `prompt` | Required for text; optional guidance for image; at most 2,000 characters |
| `imageUrl` | Required for image; one public HTTPS PNG, JPEG or WebP, maximum 10 MiB; no credentials or nonstandard ports |

The JSON body is limited to 16 KiB. Text requests must not include imageUrl. Unsupported fields are rejected before settlement. No model-tier override, multiview, styling pipeline, finished-model upload, UGC or gallery publishing is exposed. Objects use the existing default backend model; this API does not enable the P2 rollout.

Optional `Idempotency-Key` header: 1–128 letters, digits, `.`, `_`, `:`, `-`. Keep the same key, body **and original signed payment** when retrying. The payment authorization nonce also prevents duplicate dispatch if the retry key changes or is omitted. Reusing a payment or key with a different request returns 409.

Successful start (200):

```json
{"success":true,"requestId":"<request-id>","assetType":"object","jobId":"obj-<job-id>","status":"processing","statusUrl":"/api/agent/generate/obj-<job-id>/status"}
```

Poll `statusUrl` every five seconds. `X-RateLimit-Remaining` reports the remaining combined wallet allowance on a new start.

## Payment and recovery

The offer uses Base USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`, amount `2000000` (2 USDC), and the receiving wallet advertised in the 402 response. Only exact EIP-3009 `TransferWithAuthorization` payments are supported.

Unpaid POST and every GET on `/api/agent/generate` return 402 discovery. **GET never consumes payment**, even if a payment header is present. Input validation, payment verification, bounded image download, service-credit checks, and atomic capacity/wallet reservations precede settlement.

If settlement or job start becomes uncertain, the API returns **202** with a durable `requestId`, `statusUrl` and `processing` or `recovery_required` status. **Do not pay again.** Retry the original signed request, or contact Sideload support with requestId. An operator reconciles the saved authorization and backend request, then provides a recovered job URL or records a verified on-chain refund. Recovery is manual; automatic refunds are not promised. A 202 is not a completed generation.

| HTTP status | Meaning |
|---|---|
| 200 | Original or newly started job returned |
| 202 | Accepted request awaiting confirmation or operator recovery; do not send another payment |
| 400 | Invalid JSON/input/payment shape or unsafe image; not charged |
| 402 | Missing/invalid payment; discovery instructions returned |
| 409 | Payment or retry key belongs to a different request |
| 429 | Combined wallet allowance reached; see Retry-After; not charged |
| 503 | Service/capacity unavailable before settlement, or status temporarily unavailable |

## Status and results

`GET /api/agent/generate/{jobId}/status` is free. A recovery response may instead give a request-ID status URL; use that exact URL. Status values: `processing`, `completed`, `failed`, `recovery_required`, `refunded`.

Object completion:

```json
{"jobId":"obj-<job-id>","assetType":"object","status":"completed","result":{"glbUrl":"https://<asset-host>/object.glb","mml":"<m-model src=\"https://<asset-host>/object.glb\"></m-model>"}}
```

| Result field | Availability |
|---|---|
| `glbUrl` | Downloadable, self-contained GLB; completion waits for a successful full download and GLB checks |
| `mml` | Inline MML markup: Objects use `m-model`, avatars retain their existing MML |
| `vrmUrl` | Avatars only; Objects do not run rigging or VRM conversion |
| `processedImageUrl` | Optional processed reference image |

There is **no `mmlUrl` field**. Use the returned `mml` markup. On a failed new job, keep requestId for support; no second payment is required to investigate recovery.

## Client polling

```typescript
async function waitForResult(statusUrl: string) {
  for (let i = 0; i < 120; i++) {
    const response = await fetch(new URL(statusUrl, 'https://sideload.gg'));
    if (!response.ok) throw new Error(`Status unavailable: ${response.status}`);
    const job = await response.json();
    if (job.status === 'completed') return job.result;
    if (['failed', 'recovery_required', 'refunded'].includes(job.status)) {
      throw new Error(`${job.status}; contact support with ${job.requestId || job.jobId}`);
    }
    await new Promise(resolve => setTimeout(resolve, 5000));
  }
  throw new Error('Still processing; keep the original requestId and statusUrl. Do not pay again.');
}
```

For payment-enabled clients, keep the signed payment available for network retries. An x402 client that creates a new payment for every retry must use one stable Idempotency-Key to prevent a second charge; it will receive 409 and should recover the original request instead.
