BlockRun

Music Generation API

Generate full-length music tracks with lyrics, instrumental, or custom style prompts.

Generation is slow — be ready to poll

minimax/music-2.5+ produces a ~3 minute track per call regardless of duration hints, and takes 1–3 minutes to do it. Tracks that finish within 60s come back inline as 200; slower ones return 202 + poll_url and you poll until completed. Set your client timeout to at least 100 seconds per request and handle both shapes.

Endpoint

POST https://blockrun.ai/api/v1/audio/generations          # submit (returns 200 inline or 202 + poll_url)
GET  https://blockrun.ai/api/v1/audio/generations/{id}     # poll an async job (settles on first completed poll)
GET  https://blockrun.ai/api/v1/audio/models               # list music models (free, rate-limited)

Request

{
  "model": "minimax/music-2.5+",
  "prompt": "upbeat synthwave with warm neon pads and a driving beat",
  "instrumental": true,
  "duration_seconds": 30
}

Parameters

ParameterTypeRequiredDescription
modelstringNoModel ID (default: minimax/music-2.5+). The legacy id minimax/music-2.5 is remapped to minimax/music-2.5+ (same price, strict superset).
promptstringYesMusic style, mood, or description
instrumentalbooleanNoNo vocals (default: false)
lyricsstringNoCustom lyrics. Cannot combine with instrumental: true (returns 400). When omitted and not instrumental, lyrics are auto-generated.
duration_secondsintegerNoDuration hint 5–240s (default: 30), appended to the prompt as a target. The model ignores it — output is always ~3 min.

Available Models

Model IDPrice (quoted)Notes
minimax/music-2.5+$0.1585MiniMax flagship — supports lyrics, instrumental, and style prompts; ~3 min output; up to 240s billed as one track

GET /api/v1/audio/models returns the same list with billing_mode: "per_track", pricing.per_track (the $0.15 base rate before margin and fee), supports_lyrics and supports_instrumental.

How it works — inline or async

  1. POST /api/v1/audio/generations without a payment header returns a 402 challenge (headers X-Payment-Required / PAYMENT-REQUIRED / WWW-Authenticate) whose body quotes the full price and repeats the ~3 min / 1–3 min facts under generation_info.
  2. Re-send the POST with a signed X-Payment header. The gateway verifies only (no charge yet), claims the authorization's nonce so it cannot be reused, and starts generation.
  3. Fast path (finished within 60s): the track is mirrored to BlockRun storage, the payment is settled, and you get 200 with the classic { created, model, data: [...] } body plus PAYMENT-RESPONSE and X-Payment-Receipt headers.
  4. Slow path: you get 202 with { id, poll_url, status: "queued" }. Generation keeps running server-side. Poll GET {poll_url} every 3–10s with an x-payment header signed by the same wallet (a fresh signature is fine; an unsigned poll returns its own 402 challenge so x402 clients re-sign automatically). The first poll that observes completed mirrors the track, settles the payment, and returns the URL. A job that never completes is marked failed after 1 hour and is never charged.
No charge on failure

Settlement is the only step that moves USDC and it runs only after a track exists. A generation that throws, times out, or is polled as failed returns with PAYMENT-RESPONSE: {success:false} (or payment_status: "not_charged" on a poll) and nothing is charged. The gateway takes a per-job settlement lock before charging, so re-polling a finished job — or a concurrent poll — returns payment.status: "already_settled" and never bills twice.

Response

200 — inline (fast path)

{
  "created": 1775488202,
  "model": "minimax/music-2.5+",
  "data": [
    {
      "url": "https://blockrun.ai/api/media/audios/2026/08/12/<id>.mp3",
      "duration_seconds": 186,
      "lyrics": "..."
    }
  ]
}

202 — async job submitted (slow path)

{
  "id": "<job id>",
  "object": "audio.generation.job",
  "status": "queued",
  "model": "minimax/music-2.5+",
  "price": { "amount": "0.158500", "currency": "USD" },
  "payment_status": "verified",
  "created": 1775488202,
  "poll_url": "/api/v1/audio/generations/<job id>",
  "poll_instructions": "Slow model — generation exceeded the inline window. Send GET to poll_url with an x-payment header signed by the SAME wallet …"
}

GET {poll_url}202 (still generating)

{
  "id": "<job id>",
  "object": "audio.generation.job",
  "status": "in_progress",
  "model": "minimax/music-2.5+",
  "payment_status": "verified",
  "note": "Generation in progress. Poll again in 3-10s. No charge until status=completed."
}

GET {poll_url}200 (completed — charged here)

{
  "id": "<job id>",
  "object": "audio.generation.job",
  "status": "completed",
  "model": "minimax/music-2.5+",
  "created": 1775488202,
  "data": [
    { "url": "https://blockrun.ai/api/media/audios/2026/08/12/<id>.mp3", "duration_seconds": 186, "lyrics": "..." }
  ],
  "price": { "amount": "0.158500", "currency": "USD" },
  "payment": { "status": "settled", "tx_hash": "0x…", "network": "base" }
}

A re-poll returns the same body with payment.status: "already_settled". A failed job returns 200 with status: "failed", error, and payment_status: "not_charged".

Response Fields

FieldTypeDescription
createdintegerUnix timestamp
modelstringModel used
data[].urlstringPermanent BlockRun-hosted URL — the track is mirrored to BlockRun storage before settlement. Only if the mirror fails does it fall back to the upstream's expiring URL.
data[].duration_secondsintegerActual duration of the generated track (omitted if the upstream did not report one)
data[].lyricsstringGenerated lyrics (omitted for instrumental tracks)
price.amountstringAsync responses only: the amount charged, fee included
payment.statusstringAsync responses only: settled | already_settled
payment.tx_hashstringOn-chain USDC settlement tx (also in the X-Payment-Receipt header)
Save the track when convenient, not urgently

data[].url is BlockRun-hosted and does not expire. Download it whenever you like.

Examples

Via ClawRouter (recommended)

ClawRouter handles x402 payments automatically. Start it with openclaw gateway start, then call localhost:8402 directly.

curl -X POST http://localhost:8402/v1/audio/generations \
  -H "Content-Type: application/json" \
  -d '{
    "model": "minimax/music-2.5+",
    "prompt": "chill lo-fi hip hop, late night study vibes",
    "instrumental": true
  }'

Save the track:

URL=$(curl -s -X POST http://localhost:8402/v1/audio/generations \
  -H "Content-Type: application/json" \
  -d '{"model":"minimax/music-2.5+","prompt":"chill lo-fi hip hop","instrumental":true}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['data'][0]['url'])")

curl -L "$URL" -o track.mp3

Direct API (Python)

import time
import requests
from blockrun_llm.x402 import parse_payment_required, create_payment_payload
from blockrun_llm import load_wallet
from eth_account import Account

wallet_key = load_wallet()
account = Account.from_key(wallet_key)
BASE = "https://blockrun.ai"
body = {
    "model": "minimax/music-2.5+",
    "prompt": "epic orchestral film score, dramatic strings",
    "instrumental": True,
}

def sign(resp):
    """Sign the x402 challenge carried by a 402 response."""
    pr = parse_payment_required(resp.headers["PAYMENT-REQUIRED"])
    acc = pr["accepts"][0]
    return create_payment_payload(
        account=account,
        recipient=acc["payTo"],
        amount=str(acc["amount"]),
        network=acc["network"],
        resource_url=pr["resource"]["url"],
        resource_description="BlockRun music generation",
        max_timeout_seconds=acc["maxTimeoutSeconds"],
        extra=acc.get("extra"),
    )

# Step 1: get the challenge, sign it, submit (waits up to ~60s inline)
challenge = requests.post(f"{BASE}/api/v1/audio/generations", json=body)
result = requests.post(
    f"{BASE}/api/v1/audio/generations",
    json=body,
    headers={"X-Payment": sign(challenge)},
    timeout=100,
)

# Step 2: fast path returned the track inline; slow path returned 202 + poll_url
if result.status_code == 202:
    poll_url = BASE + result.json()["poll_url"]
    while True:
        time.sleep(5)
        # An unsigned poll returns its own 402 — sign it with the SAME wallet
        poll = requests.get(poll_url, timeout=100)
        if poll.status_code == 402:
            poll = requests.get(poll_url, headers={"X-Payment": sign(poll)}, timeout=100)
        job = poll.json()
        if poll.status_code == 200 and job.get("status") == "completed":
            result = poll
            break
        if job.get("status") == "failed":
            raise RuntimeError(f"generation failed, not charged: {job.get('error')}")

track = result.json()["data"][0]
print(f"Track URL: {track['url']}")
print(f"Duration: {track.get('duration_seconds')}s")
print(f"Tx: {result.headers.get('X-Payment-Receipt')}")

With Lyrics

body = {
    "model": "minimax/music-2.5+",
    "prompt": "upbeat pop with piano",
    "lyrics": "Sunshine in the morning\nBrightens up my day\nEvery step I'm taking\nLeads me on my way",
    "instrumental": False,
}
# then run the same sign → submit → (poll) loop as above

Pricing

ModelPrice/track (quoted)
minimax/music-2.5+$0.1585

Price = $0.15 × 1.05 + $0.001 — the $0.15 base rate, the 5% BlockRun media margin, and the flat $0.001 per-transaction fee charged on every paid call. That is the price.amount the 402 body quotes and the amount settled on-chain; there is no other minimum. Paid in USDC on Base (or Solana via sol.blockrun.ai).

Limitations

LimitationDetail
Output durationAlways ~3 minutes (model behaviour)
Generation time1–3 minutes per call; inline 200 only if it finishes within 60s, otherwise 202 + poll
Job lifetimeA job still queued / in_progress after 1 hour is marked failed (not charged)
FormatMP3 only
Tracks per request1
Duration controlNot supported (prompt hint ignored)
Image referenceNot supported

Error Codes

CodeDescription
400Invalid JSON or parameters (details carries the schema issues), unknown model, or conflicting lyrics + instrumental: true
402Payment required (challenge), verification failed (code: PAYMENT_INVALID default, PAYMENT_UNFUNDED for an unexecutable authorization — usually insufficient USDC on Base, PAYMENT_REPLAY for a reused authorization), or Payment settlement failed on a finished job (the track exists; retry the poll with a fresh signature — an expired authorization is the usual cause)
403Poll signed by a wallet other than the one that submitted the job (Payment payer mismatch)
404Poll for an unknown or expired job id
429Upstream rate limit (code: "RATE_LIMITED", Retry-After header)
502Upstream provider rejected the request (content policy, quota) — not charged
504Generation timed out — not charged; retry
500Server error — not charged

Every failure after a verified payment carries a PAYMENT-RESPONSE header with success: false so you can confirm nothing was settled.

Prompt Tips

Describe the style clearly:

✅ "upbeat pop with piano and synth bass, energetic chorus"
✅ "dark ambient drone, slow evolving pads, cinematic tension"
✅ "jazz trio, upright bass, brushed snare, bebop swing"

Specify mood and tempo:

✅ "120 BPM, danceable, happy summer vibes"
✅ "slow and melancholic, solo piano, minor key"

For lyrics generation (no custom lyrics):

{
  "model": "minimax/music-2.5+",
  "prompt": "pop song about chasing dreams",
  "instrumental": false
}

The model will auto-generate matching lyrics and return them in data[].lyrics.

What's next?