Claude Code Skill
Usage
Skill file
# Blowfish Token Launch Skill
You are an AI agent that can launch tokens on Solana using the Blowfish Agent API.
## API Base URL
`https://api-blowfish.neuko.ai`
## Authentication
The API uses wallet-based challenge-response auth. You need a Solana keypair.
### Auth flow
1. **Get challenge**: `POST /api/auth/challenge` with `{ "wallet": "<base58-address>" }`
- Returns `{ "nonce": "<nonce>" }`
- Nonce expires in 5 minutes
2. **Sign and verify**: Sign the message `Sign this message to authenticate: <nonce>` with ed25519, base58-encode the signature, then `POST /api/auth/verify` with:
```json
{
"wallet": "<base58-address>",
"nonce": "<nonce>",
"signature": "<base58-signature>"
}
```
- Returns `{ "token": "<jwt>" }`
- JWT expires in 15 minutes
3. **Use JWT**: Include `Authorization: Bearer <jwt>` on all subsequent requests.
### TypeScript auth example
```typescript
import { Keypair } from "@solana/web3.js";
import nacl from "tweetnacl";
import bs58 from "bs58";
async function authenticate(keypair: Keypair): Promise<string> {
const wallet = keypair.publicKey.toBase58();
const base = "https://api-blowfish.neuko.ai";
const { nonce } = await fetch(`${base}/api/auth/challenge`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wallet }),
}).then((r) => r.json());
const message = `Sign this message to authenticate: ${nonce}`;
const sig = nacl.sign.detached(new TextEncoder().encode(message), keypair.secretKey);
const { token } = await fetch(`${base}/api/auth/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wallet, nonce, signature: bs58.encode(sig) }),
}).then((r) => r.json());
return token;
}
```
## Endpoints
### Launch a token
`POST /api/v1/tokens/launch`
**Auth required.** Launches a new token on Solana via Meteora DBC. This is asynchronous -- returns an event ID for polling.
Request body:
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `name` | string | yes | 1-255 chars |
| `ticker` | string | yes | 2-10 chars, uppercase alphanumeric (`^[A-Z0-9]+$`) |
| `description` | string | no | max 1000 chars |
| `imageUrl` | string | no | max 255 chars, valid URL |
Response:
```json
{
"success": true,
"message": "We will notify you when your token is deployed",
"eventId": "uuid-v4"
}
```
Errors: `400` validation failed, `409` ticker exists, `401` auth error.
**Rate limit:** 1 launch per agent per day (UTC).
### Check launch status
`GET /api/v1/tokens/launch/status/:eventId`
**Auth required.** Poll this endpoint after launching.
Response:
```json
{
"status": "pending | success | failed | rate_limited",
"processedAt": "ISO-8601 or null"
}
```
Poll every 5-10 seconds. Stop when status is `success` or `failed`.
### List your tokens
`GET /api/v1/tokens/`
**Auth required.** Returns all tokens launched by the authenticated agent.
Response:
```json
{
"tokens": [
{
"poolAddress": "...",
"tokenMint": "...",
"ticker": "MCT",
"tokenName": "My Cool Token",
"isClaimed": false,
"claimedAt": null,
"claimedToAddress": null,
"deployedAt": "ISO-8601"
}
]
}
```
### Get a specific token
`GET /api/v1/tokens/:mintAddress`
**Auth required.** Returns a single token by its Solana mint address.
Response: Same shape as a single item in the tokens list above.
### Get claimable fees
`GET /api/v1/tokens/claims`
**Auth required.** Returns fee data for all tokens launched by the authenticated agent.
Response:
```json
{
"claims": [
{
"tokenMint": "...",
"poolAddress": "...",
"ticker": "MCT",
"dbcClaimableFees": 0.25,
"dbcTotalFees": 1.0,
"dbcClaimedFees": 0.75,
"lpClaimableFees": 0.1,
"lpTotalFees": 0.3,
"lpClaimedFees": 0.2,
"isMigrated": true
}
]
}
```
### Claim fees for a token
`POST /api/v1/tokens/claims/:mintAddress`
**Auth required.** Two-step process: get unsigned tx, sign it, submit it back.
**Step 1** -- POST without body to get unsigned transaction:
```json
{
"success": true,
"transaction": "base64-encoded-unsigned-transaction"
}
```
**Step 2** -- Sign the transaction with your agent's private key (ed25519).
**Step 3** -- POST with signed transaction:
```json
{ "signedTransaction": "base64-encoded-signed-transaction" }
```
Response:
```json
{
"success": true,
"transactionHash": "...",
"claimedSOL": 0.35
}
```
### Health check
`GET /health`
**No auth required.**
Response:
```json
{
"ok": true,
"db": "connected",
"timestamp": "ISO-8601"
}
```
## Error handling
All errors follow this pattern:
```json
{
"error": "Human-readable error message"
}
```
Common errors:
| Status | Error | Resolution |
|--------|-------|------------|
| 400 | Validation failed | Check request body against schema |
| 401 | Missing or invalid authorization header | Re-authenticate |
| 401 | Invalid or expired token | JWT expired, get a new one |
| 404 | Launch not found / Token not found | Check event ID or mint address |
| 409 | Ticker already exists | Choose a different ticker |
## Workflow
When asked to launch a token:
1. Authenticate using the wallet keypair
2. Call `POST /api/v1/tokens/launch` with token details
3. Poll `GET /api/v1/tokens/launch/status/:eventId` every 5 seconds
4. Report the final status to the user
5. On success, use `GET /api/v1/tokens/:mintAddress` to get full token detailsIntegration notes
Last updated