Skip to content

Phaser — Developer Docs

Phaser is a web3 music streaming network: artists publish HLS streams, listeners pay per stream in USDC on Base through a frictionless spend-permission flow.

This page covers everything you need to ship a Phaser-powered app. Two npm packages (@jackmorgan/phaser-player, @jackmorgan/phaser-catalog) plus a REST API at https://phaser-api.jackmorgan.xyz.


Architecture at a glance

                                 ┌───────────────────────────────┐
                                 │  Phaser REST API              │
                                 │  phaser-api.jackmorgan.xyz    │
                                 │  (Hono · Neon · S3)           │
                                 └──────────┬────────────────────┘

                 ┌──────────────────────────┼──────────────────────────┐
                 │                          │                          │
                 ▼                          ▼                          ▼
   ┌──────────────────────────────┐  ┌───────────────────────────────┐  ┌──────────────────────┐
   │  @jackmorgan/phaser-player   │  │  @jackmorgan/phaser-catalog   │  │   Direct REST        │
   │                              │  │                               │  │   (any language)     │
   │  HLS playback +              │  │  Typed client for             │  │                      │
   │  wallet-gated streams        │  │  tracks/artists/etc           │  │  GET /v1/*           │
   │  (viem + hls.js)             │  │  (zero deps)                  │  │                      │
   └──────────────────────────────┘  └───────────────────────────────┘  └──────────────────────┘
PieceInstallUse it when…
@jackmorgan/phaser-playernpm install @jackmorgan/phaser-playeryou want to play Phaser tracks (headless or drop-in web component)
@jackmorgan/phaser-catalognpm install @jackmorgan/phaser-catalogyou want to browse/search the catalog without hand-rolling fetch calls
REST APIyou're building in a language that doesn't have a Phaser SDK yet

@jackmorgan/phaser-player

Embeddable HLS music player. Handles wallet connection, USDC payment via spend permissions, and HLS playback in one self-contained bundle. hls.js and viem are bundled, so there's nothing to peer-dep.

Two entry points:

  • Player — headless class, bring your own UI
  • <phaser-player> — web component with a themeable default UI

Install

sh
npm install @jackmorgan/phaser-player

Or drop in the IIFE build from a CDN:

html
<script src="https://unpkg.com/@jackmorgan/phaser-player"></script>
<script>
  const player = new Phaser.Player();
</script>

Web component (easiest)

html
<script type="module">
  import "@jackmorgan/phaser-player";
</script>

<phaser-player
  track-id="trk_abc123"
  theme="dark"
></phaser-player>

Attributes map 1:1 to PlayerOptions. The underlying Player instance is available as element.player once connected.

Headless

ts
import { Player } from "@jackmorgan/phaser-player";

const player = new Player();

player.on("track:loaded", (e) => console.log(e.title));
player.on("stream:progress", (e) => console.log(e.currentTime));

await player.load("trk_abc123");
await player.connectWallet();  // paid tracks require a wallet first
await player.play();

Player API

MethodReturnsNotes
load(trackId)Promise<TrackInfo>fetches metadata
play()Promise<void>handles payment + HLS start
pause() / resume() / stop()void
seek(seconds) / setVolume(0..1)void
connectWallet()Promise<string>returns the address
disconnectWallet() / getBalance()
setQueue(ids) · next() · previous()queue walks; previous() restarts if >3s elapsed
getSpendPermission()Promise<SpendPermissionState>
on(event, handler) / off(...)see event table below
destroy()voidtear down

Read-only state: player.state (full snapshot), player.track (current TrackInfo \| null).

Events

ts
player.on("stream:progress", ({ currentTime, duration }) => { … });
EventPayload
wallet:connected{ address }
wallet:disconnected{}
payment:required{ trackId, price }
payment:processing{ trackId, txHash? }
payment:confirmed{ trackId, chargeId, amount }
payment:failed{ trackId, error }
payment:permission{ status, remainingUsdc }
track:loaded{ trackId, title, artist, duration, price }
stream:started{ trackId, title }
stream:progress{ currentTime, duration }
stream:paused / stream:resumed / stream:ended{ trackId }
error{ code, message }

Bring-your-own wallet

If you already have a wallet flow (wagmi, RainbowKit, your own stack), pass an AuthAdapter to skip the built-in viem module:

ts
const player = new Player({
  auth: myAdapter,  // must satisfy the AuthAdapter interface
});

Minimum AuthAdapter shape (full doc in the package README):

ts
interface AuthAdapter {
  readonly address: string | null;
  readonly isConnected: boolean;
  readonly jwt: string | null;
  signMessage(msg: string): Promise<string>;
  signTypedData(typed: any): Promise<string>;
  sendTransaction(tx: {...}): Promise<string>;
  getBalance(): Promise<string>;
  on(ev, fn): void;
  off(ev, fn): void;
}

If auth.jwt is already populated, the player skips its SIWE round-trip.


@jackmorgan/phaser-catalog

Typed REST client. Zero runtime deps. Works in Node 20+ and every modern browser (uses global fetch).

Install

sh
npm install @jackmorgan/phaser-catalog

Quick start

ts
import { CatalogClient } from "@jackmorgan/phaser-catalog";

const catalog = new CatalogClient();

const page = await catalog.tracks({ q: "neon", sort: "newest", limit: 25 });
const genres = await catalog.genres();
const results = await catalog.search("daft punk");

Methods

Every method accepts an optional AbortSignal.

ts
catalog.tracks({ q?, genre?, artistId?, sort?, cursor?, limit? })  // → Paginated<Track>
catalog.track(id)                                                   // → Track
catalog.artists({ q?, cursor?, limit? })                            // → Paginated<Artist>
catalog.artist(id)                                                  // → Artist
catalog.artistTracks(id, { cursor?, limit? })                       // → Paginated<Track>
catalog.album(id)                                                   // → Album
catalog.genres()                                                    // → Genre[]
catalog.search(q, { limit? })                                       // → SearchResult
catalog.playlist(id)                                                // → Playlist

sort accepts "newest" | "popular". All paginated responses are { data: T[]; next_cursor: string \| null; has_more: boolean }.

Errors

Non-2xx responses throw a typed error:

ts
import {
  CatalogError,
  CatalogAuthError,
  CatalogNotFoundError,
  CatalogRateLimitError,
} from "@jackmorgan/phaser-catalog";

try {
  await catalog.track("trk_missing");
} catch (err) {
  if (err instanceof CatalogNotFoundError)   { /* 404 */ }
  else if (err instanceof CatalogAuthError)  { /* 401 / 403 */ }
  else if (err instanceof CatalogRateLimitError) { console.log(err.retryAfter); }
  else if (err instanceof CatalogError)      { console.log(err.status, err.code); }
}

Every error carries { status, code, message, url?, body? }. Network failures (DNS, reset) propagate as the underlying TypeError / AbortError from fetch.

Cancellation

ts
const ac = new AbortController();
const promise = catalog.tracks({ q: "neon", signal: ac.signal });
ac.abort();

Pairing with the player

ts
import { CatalogClient } from "@jackmorgan/phaser-catalog";
import { Player } from "@jackmorgan/phaser-player";

const catalog = new CatalogClient();
const player  = new Player();

const page = await catalog.tracks({ sort: "newest" });
await player.load(page.data[0].id);
await player.play();

REST API

Base URL: https://phaser-api.jackmorgan.xyz. All endpoints live under /v1/*.

Auth

Public catalog and streaming endpoints are open — no header required.

HeaderValue
Authorization: Bearer <jwt>SIWE JWT — required on /v1/me/* and artist upload routes

Response shape

Paginated endpoints: { data: T[], next_cursor: string \| null, has_more: boolean }. Single-resource endpoints return the resource directly. Errors: { error: string, code?: string } with an HTTP status code.

Endpoints (public)

MethodPathPurpose
GET/v1/trackslist tracks (q, genre, artist_id, sort, cursor, limit)
GET/v1/tracks/:idsingle track
GET/v1/artistslist artists
GET/v1/artists/:idsingle artist
GET/v1/artists/:id/trackstracks by artist
GET/v1/albums/:idsingle album (with tracks)
GET/v1/genresgenre list
GET/v1/search?q=…tracks + artists + albums
GET/v1/playlists/:idsingle playlist (public only)

Endpoints (auth — SIWE)

MethodPathPurpose
POST/v1/auth/challengefetch SIWE message to sign
POST/v1/auth/verifysubmit signature → returns JWT
GET/v1/me/likesthe caller's liked tracks
PUT/v1/me/likes/:trackIdlike a track
DELETE/v1/me/likes/:trackIdunlike
GET / POST/v1/me/playlistsplaylist CRUD (see packages/api/src/routes/me.ts)

Endpoints (streaming + uploads)

MethodPathPurpose
POST/v1/stream/:trackId/authorizecharges the spend permission, returns an HLS URL
GET/v1/stream/…/playlist.m3u8HLS manifest (token-gated)
POST/v1/upload/draftartist: create a draft track
POST/v1/upload/:id/presignartist: S3 presign for the audio file
POST/v1/upload/:id/finalizeartist: transcode + publish

Day-to-day consumers almost never hit streaming/upload endpoints directly — the player does the streaming handshake for you, and the upload flow is a dashboard feature. They're documented here so you know what's available.

Example — cURL

sh
# List 10 newest tracks
curl "https://phaser-api.jackmorgan.xyz/v1/tracks?sort=newest&limit=10"

# Search
curl "https://phaser-api.jackmorgan.xyz/v1/search?q=daft+punk"

Support

Released under the MIT License.