Skill: basedmereum/blowfish-launch (version 1.0.0)

--- name: blowfish-launch description: Launch tokens on Solana via the Blowfish Agent API (Meteora Dynamic Bonding Curve). Use when the user wants to launch, deploy, or create a token on Solana, check token launch status, list launched tokens, or claim trading fees. Triggers on "launch token", "deploy token", "create token", "blowfish launch", "check launch status", "claim fees". --- # Blowfish Token Launch Launch tokens on Solana programmatically via the Blowfish Agent API. **Base URL:** `https://api-blowfish.neuko.ai` ## Prerequisites - A Solana keypair (ed25519). The private key should be available as `WALLET_SECRET_KEY` env var (JSON array of bytes). - Node.js 18+ or Bun with `@solana/web3.js`, `tweetnacl`, `bs58` ## Workflow 1. **Authenticate** — wallet-based challenge-response → JWT (15 min expiry) 2. **Launch** — POST token params → receive `eventId` 3. **Poll** — GET status by `eventId` until `success` or `failed` 4. **Done** — token is live on Solana via Meteora DBC ## Quick Launch Run the bundled script: ```bash WALLET_SECRET_KEY='[...]' bun run scripts/blowfish-launch.ts \ --name "My Token" \ --ticker "MYTK" \ --description "Optional description" \ --imageUrl "https://example.com/logo.png" ``` ## API Endpoints ### Authentication - `POST /api/auth/challenge` — `{ wallet }` → `{ nonce }` - `POST /api/auth/verify` — `{ wallet, nonce, signature }` → `{ token }` Sign message: `Sign this message to authenticate: <nonce>` with ed25519, base58-encode signature. ### Tokens - `POST /api/v1/tokens/launch` — `{ name, ticker, description?, imageUrl? }` → `{ eventId }` (Bearer auth) - `GET /api/v1/tokens/launch/status/:eventId` — poll until `success`/`failed`/`rate_limited` - `GET /api/v1/tokens/` — list your tokens - `GET /api/v1/tokens/:id` — get specific token ### Fee Claiming - `GET /api/v1/claims/` — get eligible claims - `POST /api/v1/claims/:tokenId` — claim fees for a token ## Token Parameters | Field | Rules | |-------|-------| | `name` | 1-255 chars, required | | `ticker` | 2-10 chars, `^[A-Z0-9]+$`, required | | `description` | max 1000 chars, optional | | `imageUrl` | max 255 chars, optional | ## Error Handling - **409** — ticker taken, choose another - **401** — JWT expired, re-authenticate - **Rate limit** — 1 launch per agent per UTC day ## Full API Reference See [references/api.md](references/api.md) for complete endpoint documentation.

path: references/api.md
content: # Blowfish Agent API Reference

Base URL: `https://api-blowfish.neuko.ai`

## Authentication

### POST /api/auth/challenge
Request a nonce for wallet authentication.

**Request:**
```json
{ "wallet": "<base58-solana-address>" }
```

**Response:**
```json
{ "nonce": "<server-generated-nonce>" }
```

Nonce TTL: 5 minutes. Each new challenge overwrites the previous nonce for that wallet.

### POST /api/auth/verify
Verify signature and receive JWT.

**Request:**
```json
{
  "wallet": "<base58-solana-address>",
  "nonce": "<nonce-from-challenge>",
  "signature": "<base58-encoded-ed25519-detached-signature>"
}
```

Message to sign: `Sign this message to authenticate: <nonce>`

**Response:**
```json
{ "token": "<jwt>" }
```

JWT: HS256, 15-min expiry, sub=wallet, scope=["read","trade"]

## Tokens

All token endpoints require `Authorization: Bearer <jwt>`.

### POST /api/v1/tokens/launch
Launch a new token on Solana via Meteora DBC.

**Request:**
```json
{
  "name": "My Token",
  "ticker": "MYTK",
  "description": "Optional, max 1000 chars",
  "imageUrl": "https://example.com/logo.png"
}
```

| Field | Required | Constraints |
|-------|----------|-------------|
| name | Yes | 1-255 characters |
| ticker | Yes | 2-10 chars, `^[A-Z0-9]+$` |
| description | No | Max 1000 characters |
| imageUrl | No | Max 255 characters, direct image URL |

**Response:**
```json
{ "eventId": "<uuid>" }
```

**Errors:**
- 409: Ticker already taken
- 429/rate_limited status: 1 launch per agent per UTC day

### GET /api/v1/tokens/launch/status/:eventId
Check launch status.

**Response:**
```json
{ "status": "pending" | "processing" | "success" | "failed" | "rate_limited", ... }
```

Poll every 5 seconds. On success, token is live on Solana.

### GET /api/v1/tokens/
List all tokens launched by the authenticated wallet.

### GET /api/v1/tokens/:id
Get details for a specific token.

## Fee Claiming

### GET /api/v1/claims/
Get all eligible fee claims for your tokens.

### POST /api/v1/claims/:tokenId
Claim accumulated trading fees for a specific token.

## System

### GET /health
Health check (no auth required).

## HTTP Status Codes

| Code | Meaning |
|------|---------|
| 200 | Success |
| 400 | Bad request / validation failure |
| 401 | Unauthorized / expired JWT |
| 404 | Not found |
| 409 | Conflict (duplicate ticker) |
| 500 | Internal server error |
| 502 | Bad gateway (on-chain tx build failure) |
| 503 | Service unavailable |

## Response Format

Success: `{ "success": true, "data": "..." }`
Error: `{ "error": "Human-readable message" }`


path: scripts/blowfish-launch.ts
content: #!/usr/bin/env bun
import { Keypair } from "@solana/web3.js";
import nacl from "tweetnacl";
import bs58 from "bs58";
import { parseArgs } from "util";

const API_BASE = "https://api-blowfish.neuko.ai";

// --- Args ---
const { values } = parseArgs({
  args: Bun.argv.slice(2),
  options: {
    name: { type: "string" },
    ticker: { type: "string" },
    description: { type: "string", default: "" },
    imageUrl: { type: "string", default: "" },
  },
  strict: true,
});

if (!values.name || !values.ticker) {
  console.error("Usage: blowfish-launch.ts --name <name> --ticker <TICKER> [--description <desc>] [--imageUrl <url>]");
  process.exit(1);
}

// --- Auth ---
async function authenticate(keypair: Keypair): Promise<string> {
  const wallet = keypair.publicKey.toBase58();

  const challengeRes = await fetch(`${API_BASE}/api/auth/challenge`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ wallet }),
  });
  const { nonce } = await challengeRes.json();

  const message = `Sign this message to authenticate: ${nonce}`;
  const sig = nacl.sign.detached(new TextEncoder().encode(message), keypair.secretKey);

  const verifyRes = await fetch(`${API_BASE}/api/auth/verify`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ wallet, nonce, signature: bs58.encode(sig) }),
  });
  const { token } = await verifyRes.json();
  return token;
}

// --- Launch ---
async function launchToken(
  [REDACTED_SECRET],
  params: { name: string; ticker: string; description?: string; imageUrl?: string }
): Promise<string> {
  const response = await fetch(`${API_BASE}/api/v1/tokens/launch`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify(params),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Launch failed (${response.status}): ${error.error}`);
  }

  const { eventId } = await response.json();
  return eventId;
}

// --- Poll ---
async function waitForDeployment(eventId: string, [REDACTED_SECRET]): Promise<string> {
  const url = `${API_BASE}/api/v1/tokens/launch/status/${eventId}`;

  for (let i = 0; i < 60; i++) {
    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${token}` },
    });
    const data = await response.json();

    if (data.status === "success") {
      console.log("✅ Token launched successfully!");
      console.log(JSON.stringify(data, null, 2));
      return "success";
    }
    if (data.status === "failed" || data.status === "rate_limited") {
      throw new Error(`Launch ${data.status}: ${JSON.stringify(data)}`);
    }

    console.log(`⏳ Status: ${data.status}... (attempt ${i + 1}/60)`);
    await new Promise((resolve) => setTimeout(resolve, 5000));
  }

  throw new Error("Launch timed out after 5 minutes");
}

// --- Main ---
async function main() {
  const secretKey = Uint8Array.from(JSON.parse(process.env.WALLET_SECRET_KEY!));
  const keypair = Keypair.fromSecretKey(secretKey);

  console.log(`🔐 Authenticating wallet: ${keypair.publicKey.toBase58()}`);
  const token = await authenticate(keypair);

  const params: any = { name: values.name!, ticker: values.ticker! };
  if (values.description) params.description = values.description;
  if (values.imageUrl) params.imageUrl = values.imageUrl;

  console.log(`🚀 Launching [REDACTED_SECRET] ($${params.ticker})`);
  const eventId = await launchToken(token, params);
  console.log(`📋 Event ID: ${eventId}`);

  await waitForDeployment(eventId, token);
}

main().catch((err) => {
  console.error(`❌ ${err.message}`);
  process.exit(1);
});


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (HIGH)