> For the complete documentation index, see [llms.txt](https://docs.kumbaya.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kumbaya.xyz/developers/apis/client-api.md).

# Client API

The Client API powers the social side of Kumbaya: user accounts, sessions, comments, fuel/tipping, launches, and the **token claim** flow that links on-chain creators to Kumbaya profiles.

| Field             | Value                                                                                                                |
| ----------------- | -------------------------------------------------------------------------------------------------------------------- |
| Base URL          | `https://clients.kumbaya.xyz`                                                                                        |
| Base path         | `/v1`                                                                                                                |
| OpenAPI / Swagger | [`clients.kumbaya.xyz/docs`](https://clients.kumbaya.xyz/docs)                                                       |
| Auth              | Mostly JWT (Privy-issued) via `Authorization: Bearer` or cookies. Public for some reads. Signed-message for `claim`. |

> **Building an LLM agent?** These endpoints are also available as MCP tools - 76 of them under the `app_` prefix. See the [MCP Server](/developers/building-agents/agent-kit/mcp.md), part of the [Kumbaya Agent Kit](/developers/building-agents/agent-kit.md).

## Endpoint groups

### Sessions (`/v1/session/*`)

| Endpoint                             | Auth            | Purpose                                     |
| ------------------------------------ | --------------- | ------------------------------------------- |
| `POST /v1/session/create`            | Privy `idToken` | Create a session from a Privy login         |
| `GET /v1/session/current`            | JWT             | Fetch current session                       |
| `POST /v1/session/refresh`           | JWT             | Rotate token                                |
| `POST /v1/session/logout`            | JWT             | Revoke session                              |
| `POST /v1/session/wallet-state`      | Optional JWT    | Update wallet state metadata                |
| **`GET /v1/session/wallet/nonce`**   | None            | **SIWE: get a one-time nonce for a wallet** |
| **`POST /v1/session/wallet/verify`** | None            | **SIWE: verify signed message, return JWT** |

#### Sign-In With Ethereum (SIWE) - for agents and wallet-only users

The Client API supports **EIP-4361 (Sign-In With Ethereum)** as a first-class auth path alongside Privy. This is the right path for AI agents, bots, and any integration that has its own private key and doesn't want to go through a social login flow.

**1. Request a nonce**

```http
GET /v1/session/wallet/nonce?address=0xYOUR_WALLET
```

Response: `{ "nonce": "abc123…" }`. The nonce is valid for **5 minutes** and is single-use.

**2. Build a SIWE message** (EIP-4361). The parser is minimal - it requires the address and `Nonce: …` fields:

```
kumbaya.xyz wants you to sign in with your Ethereum account:
0xYOUR_WALLET

Sign in to Kumbaya.

URI: https://kumbaya.xyz
Version: 1
Chain ID: 4326
Nonce: <nonce-from-step-1>
Issued At: 2026-01-30T12:00:00Z
```

Sign it with `personal_sign` (most wallets/SDKs do this by default for raw strings).

**3. Verify**

```http
POST /v1/session/wallet/verify
Content-Type: application/json

{
  "message":   "<the SIWE string above, verbatim>",
  "signature": "0x..."
}
```

Response:

```jsonc
{
  "token":     "<jwt>",
  "expiresAt": "2026-02-29T...",
  "user": {
    "id":             "cuid...",
    "walletAddress":  "0xYourWallet",
    "name":           null,
    "image":          null
  }
}
```

The same JWT is also set as an httpOnly cookie. Use either path on subsequent requests.

**Wallet-only user creation.** If the wallet has never logged in before (no Privy user exists with that address), the backend creates a new user record with a synthetic `privyDid: "wallet:0x..."`. This means **agents can self-onboard** - no admin, no Privy account, no email needed. From there the wallet has the same access as any Privy-backed account except for features that need Privy-specific data (which are rare).

### Users (`/v1/users/*`)

| Endpoint                                                         | Auth         | Purpose                  |
| ---------------------------------------------------------------- | ------------ | ------------------------ |
| `GET /v1/users/me`                                               | JWT          | Current user's profile   |
| `GET /v1/users/profile/address/{wallet}`                         | Public       | Public profile by wallet |
| `GET /v1/users/username/check?username=`                         | Public       | Username availability    |
| `PATCH /v1/users/profile`                                        | JWT          | Update name/bio/image    |
| `POST /v1/users/profile/image`                                   | JWT          | Upload avatar            |
| `GET /v1/users/stats` / `activity` / `yaps` / `fuels` / `trades` | Optional JWT | Profile widgets          |
| `PATCH /v1/users/admin/{userId}` and similar                     | Admin JWT    | Moderation               |

### Tokens (`/v1/tokens/*`)

The token endpoints back the launchpad detail page **and** the claim flow.

| Endpoint                                    | Auth                        | Purpose                                               |
| ------------------------------------------- | --------------------------- | ----------------------------------------------------- |
| `GET /v1/tokens/{mintAddress}`              | Public                      | Token detail (creator, holders, metadata)             |
| `GET /v1/tokens/{mintAddress}/buyers`       | Public                      | Holder leaderboard                                    |
| `GET /v1/tokens/{mintAddress}/buyers/chart` | Public                      | Buyer distribution by time                            |
| `GET /v1/tokens/{mintAddress}/fuels`        | Public                      | Fuel tip history                                      |
| `GET /v1/tokens/{mintAddress}/position`     | JWT                         | Caller's position                                     |
| `POST /v1/tokens/batch/images`              | Public                      | Batch fetch icons                                     |
| `POST /v1/tokens/{mintAddress}/report`      | JWT                         | Report a token                                        |
| **`POST /v1/tokens/{mintAddress}/claim`**   | **JWT + EIP-712 signature** | **Claim an unclaimed token (see below)**              |
| `POST /v1/tokens/{mintAddress}/claim/image` | JWT                         | Upload claim image (separate from the metadata claim) |

#### Token claim - EIP-712 signature

The user must be **authenticated** (JWT) *and* present an **EIP-712 signature** that recovers to the on-chain `creator` of the token. The signature is verified server-side; only on success does the backend create the `TokenMetadata` record linking the token to the authenticated user.

**Request body** (`ClaimTokenInputSchema`):

```jsonc
{
  "chainId":     4326,
  "description": "What this token is about (1–500 chars)",
  "category":    "MEMES",                 // 'MEMES' | 'DARES'
  "signature":   "0x...",                  // EIP-712 signature
  "signedAt":    1740000000,               // Unix timestamp the user signed at
  "nonce":       "random-string",          // Unique per signing session
  "website":     "yourcoin.com",           // optional
  "xHandle":     "yourcoin",               // optional, ≤15 chars
  "telegramUrl": "t.me/yourcoin"           // optional
}
```

`name`, `symbol`, and the image are **not** in this body. Name and symbol are read from the on-chain token; the image is uploaded via the separate `POST /v1/tokens/{mintAddress}/claim/image` endpoint after the metadata claim succeeds.

**EIP-712 typed data** the user signs:

```ts
domain = {
  name:    'Kumbaya Token Claim',
  version: '1',
  chainId,                         // numeric chain id
}
types = {
  ClaimListing: [
    { name: 'mintAddress', type: 'address' },
    { name: 'chainId',     type: 'uint256' },
    { name: 'timestamp',   type: 'uint256' },   // matches `signedAt` in the body
    { name: 'nonce',       type: 'string'  },
  ],
}
primaryType = 'ClaimListing'
message = { mintAddress, chainId, timestamp: signedAt, nonce }
```

The signature must be produced within the **last hour** - `signedAt` older than 3600 seconds rejects with `SIGNATURE_EXPIRED`.

**Error codes** (from `ClaimErrorCodes`):

| Code                | When                                                      |
| ------------------- | --------------------------------------------------------- |
| `UNAUTHORIZED`      | No valid session                                          |
| `TOKEN_NOT_FOUND`   | No on-chain token at this address                         |
| `ALREADY_CLAIMED`   | Listing already has a `creatorId`                         |
| `TOKEN_DELETED`     | Token was removed by an admin                             |
| `INVALID_SIGNATURE` | Signature doesn't validate against the EIP-712 typed data |
| `NOT_CREATOR`       | Recovered signer ≠ on-chain `creator`                     |
| `SIGNATURE_EXPIRED` | `signedAt` older than 1h                                  |
| `IMAGE_REQUIRED`    | (image endpoint) no image provided                        |

For the user-facing version of this flow see [**client docs › Unclaimed tokens**](https://github.com/Kumbaya-xyz/documentation/tree/main/client/launchpad/unclaimed-tokens.md).

### Launches (`/v1/launch/*`)

Used by the frontend to track launch state across drafts → on-chain deployment. The actual `FireLaunch.ignite()` transaction is signed and sent from the user's wallet; the Client API tracks the metadata flow around it.

| Endpoint                      | Auth | Purpose                                                                                      |
| ----------------------------- | ---- | -------------------------------------------------------------------------------------------- |
| `POST /v1/launch`             | JWT  | Create a draft launch (body: `name`, `symbol`, `description?`, `category`, `chainId`)        |
| `GET /v1/launch/pending`      | JWT  | Caller's launches not yet `COMPLETED` or `FAILED`                                            |
| `GET /v1/launch/{id}`         | JWT  | Launch detail                                                                                |
| `POST /v1/launch/{id}/image`  | JWT  | Upload launch image (transitions `DRAFT` → `IMAGE_UPLOADED`)                                 |
| `POST /v1/launch/{id}/submit` | JWT  | Submit on-chain `tokenAddress` for verification (transitions `IMAGE_UPLOADED` → `COMPLETED`) |
| `POST /v1/launch/{id}/fail`   | JWT  | Mark a launch `FAILED`                                                                       |
| `DELETE /v1/launch/{id}`      | JWT  | Delete a draft                                                                               |

#### Status state machine

```
DRAFT ──────► IMAGE_UPLOADED ──────► COMPLETED
                    │                    
                    ▼                    
                 FAILED   (or via /fail)
```

`TokenLaunchStatus = 'DRAFT' | 'IMAGE_UPLOADED' | 'COMPLETED' | 'FAILED'`.

#### Create launch body (`CreateLaunchInputSchema`)

```jsonc
{
  "name":        "My Token",                // 1–32 chars
  "symbol":      "MTK",                     // 1–10 chars, uppercase alphanumeric only
  "description": "A short prompt...",       // 8–500 chars, optional
  "category":    "MEMES",                    // 'MEMES' | 'DARES', default MEMES
  "chainId":     4326
}
```

A user can have at most one in-flight launch per `(chainId, symbol)` pair - duplicates return `400 { code: 'DUPLICATE_LAUNCH' }`.

#### Submit launch body (`SubmitLaunchInputSchema`)

```jsonc
{ "tokenAddress": "0x..." }   // the on-chain FireToken address from FireLaunch.ignite() receipt
```

The backend verifies that the authenticated user matches the on-chain `creator` for the supplied address before creating a `TokenMetadata` record. The launch must be in `IMAGE_UPLOADED` status, and the corresponding token must not already have a `TokenMetadata` record.

### Comments (`/v1/comments/*`)

Standard CRUD for token comment threads (Yaps).

| Endpoint                                                  | Auth   | Purpose                            |
| --------------------------------------------------------- | ------ | ---------------------------------- |
| `GET /v1/comments/tokens/{mintAddress}`                   | Public | Comments on a token (paginated)    |
| `GET /v1/comments/{id}`                                   | Public | Single comment + replies           |
| `GET /v1/comments/tokens/{mintAddress}/post/{postNumber}` | Public | Comment by 4chan-style post number |
| `POST /v1/comments`                                       | JWT    | Create comment                     |
| `PUT /v1/comments/{id}`                                   | JWT    | Edit own comment                   |
| `DELETE /v1/comments/{id}`                                | JWT    | Delete own comment                 |
| `POST /v1/comments/{id}/report`                           | JWT    | Report comment                     |

### Engagement (`/v1/likes`, `/v1/dislike`, `/v1/favorites`)

`POST` toggles for likes/dislikes/favorites on posts and comments.

### Fuel (`/v1/fuel/*`)

| Endpoint                | Auth | Purpose               |
| ----------------------- | ---- | --------------------- |
| `GET /v1/fuel/credits`  | JWT  | Caller's fuel balance |
| `GET /v1/fuel/received` | JWT  | Inbound transactions  |
| `GET /v1/fuel/given`    | JWT  | Outbound transactions |

### Gifts (`/v1/gifts/*`)

| Endpoint                     | Auth  | Purpose                    |
| ---------------------------- | ----- | -------------------------- |
| `GET /v1/gifts/status`       | JWT   | Caller's gift cycle status |
| `GET /v1/gifts/prepare`      | JWT   | Prepare gift claim         |
| `POST /v1/gifts`             | JWT   | Claim gift                 |
| `POST /v1/gifts/reset-cycle` | Admin | Reset gift cycle           |

### Notifications & push (`/v1/notifications/*`, `/v1/push/*`)

Standard endpoints for notification list, unread count, mark-read, and Web Push subscription registration. `GET /v1/push/vapid-key` returns the public VAPID key for browser registration.

### Feed (`/v1/feed/*`)

| Endpoint                   | Auth         | Purpose                                                                          |
| -------------------------- | ------------ | -------------------------------------------------------------------------------- |
| `GET /v1/feed`             | Optional JWT | Social feed (`type=ALL\|LAUNCHES\|YAPS\|FUELS`), personalized when authenticated |
| `GET /v1/feed/content`     | None         | Content feed (yaps and shills)                                                   |
| `GET /v1/feed/dares/viral` | None         | Trending dare tokens                                                             |
| `GET /v1/feed/sidebar`     | None         | Sidebar widgets                                                                  |

### Content & discovery (`/v1/content/*`, `/v1/competition`, `/v1/badges`, `/v1/shares`)

| Endpoint                                                      | Auth | Purpose                         |
| ------------------------------------------------------------- | ---- | ------------------------------- |
| `GET /v1/content/yaps` / `shills` / `tips` / `landing-ticker` | None | Public content streams          |
| `GET /v1/competition/stats`                                   | None | Competition leaderboards        |
| `GET /v1/badges` / `GET /v1/badges/{badgeId}`                 | None | Badge catalog                   |
| `POST /v1/shares`                                             | JWT  | Create a share link for a token |

### Misc

* `GET /healthz` - liveness
* `GET /v1/ip` - return caller IP
* `POST /v1/x-auth/store-tokens` - store X OAuth tokens

## Auth model

* **JWT** is issued by `POST /v1/session/create` (validating a Privy idToken or wallet signature) and stored as an httpOnly cookie. Subsequent requests use the cookie or `Authorization: Bearer <jwt>`.
* **Signed-message auth** for `POST /v1/tokens/{mintAddress}/claim` is independent of the session - the request must include a wallet-signed payload that recovers to the on-chain creator address.
* **Admin endpoints** require `isAdmin: true` on the authenticated user and otherwise behave like any other JWT-protected route.

For complete request/response shapes consult the OpenAPI Swagger UI at [`clients.kumbaya.xyz/docs`](https://clients.kumbaya.xyz/docs).
