I went to Vercel Ship LDN in June and came away inspired by their new capability Eve, the agent framework they announced on stage. Eve is filesystem-first: an agent is a directory of files, and the framework handles durability, streaming, tools, and deployment. They called it "Next.js for agents" and having spent a few weeks with it, that's a fair description. I decided it would be fun to build something around the capability.

Music has always been a passion. I also have a pair of Technics 1210s, stacks of vinyl, and a more modern Traktor setup that gets used. So when I was thinking about what to build with Eve, the idea of an AI radio DJ felt like a natural fit: give it a brief, let it crate-dig a tracklist, write some patter between songs, and play the whole thing through Spotify.

There was a practical motivation too. Spotify's recommendation algorithm is fine for surfacing more of what you already listen to, but it's not great at introducing entirely new artists or songs outside your usual orbit. I wanted something that could take a mood or a theme and pull from a wider pool; and critically, let me exclude tracks or artists I already know so the results are genuinely fresh.

What it does

You give the app a brief: a genre, a mood, a decade, an occasion, whatever you want. The Eve agent takes that brief and programmes a themed radio show: an ordered set of real Spotify tracks with DJ patter between each one, structured with an energy arc so the show has shape rather than being a flat playlist. The Next.js player streams the build progress in real time, then plays the tracks through Spotify Connect with transport controls, playlist saving, and the option to save individual tracks to your Liked Songs.

The exclusion feature is the part I find most useful day to day. When you toggle it on, the agent checks each candidate track against your Liked Songs and your top tracks across all three Spotify time ranges (short, medium, and long term). The agent filters those out during crate-digging, so what comes back is music you haven't heard before rather than variations on what Spotify already knows you like.

There's also a "Surprise me" mode. It picks a random track from your Liked Songs as the seed, then builds a discovery show around it: same vibe, era, or emotional shape, but with the remaining slots filled by tracks you haven't heard. It's a nice way to find new music anchored to something you already love.

The stack

The app is a single Next.js project wrapped with Eve's withEve helper and deployed on Vercel. The key components:

  • Eve agent: handles show generation; instructions in markdown, tools in TypeScript, skills for energy-arc planning and DJ patter writing
  • AI Gateway: routes model calls through Vercel's infrastructure using OIDC, so there are no provider API keys to manage
  • Spotify Web API + Web Playback SDK: PKCE auth flow for the user, Connect for device playback
  • Vercel WAF: rate limiting keyed on the authenticated Spotify user ID
Browser Next.js player Vercel /api/show Spotify token gate Eve agent Durable session AI Gateway Spotify Web API Search + playback Vercel WAF Rate limit (user ID) Spotify Connect Device playback

The withEve wrapper is worth highlighting. It proxies the Eve agent same-origin behind the Next.js app, so the browser's auth cookie reaches it and the internal call from /api/show to Eve's /eve/v1/session endpoint stays on Vercel's private network. This avoids the need for a separate agent deployment or an external API gateway.

The agent

The Eve agent is structured around three concerns: planning the show's shape, finding real tracks, and writing the links between them.

The agent's instructions are a single markdown file that describes how to build a show. It plans an energy arc first: how many tracks, how the energy rises and falls across them. Then for each slot in the arc it proposes a track from its own musical knowledge and calls search_tracks to resolve it to a real, playable Spotify URI. The agent's taste is the creative input; the tool confirms the track exists, checks whether it's playable in the listener's market, and flags whether it's already in their library.

Two skills support this. The build_energy_arc skill guides the agent through structuring the show's shape: openers, builds, peaks, cooldowns. The write_dj_patter skill sets the tone for the spoken links; they should feel like a real radio DJ rather than a track listing.

The search_tracks tool uses toModelOutput to slim the Spotify API response down to a numbered shortlist with exclusion flags. This keeps the model's context lean when it's making 8-10 search calls per show; it sees "1. Portishead — Glory Box (already in the listener's library — avoid) [spotify:track:...]" rather than the full API payload. The full result is still available to the frontend via the session stream.

The finalize_show tool validates the complete show against a shared Zod schema and emits it as structured output. This schema lives in lib/show-schema.ts and is imported by both the agent tool and the frontend, so both sides agree on the shape of a show.

Streaming the build

Eve sessions are durable: they run on Vercel Workflows with each step checkpointed, so a session survives cold starts and redeploys. The /api/show route creates a session, then attaches to its NDJSON event stream and translates Eve's lifecycle events into a simpler wire protocol for the browser: session (the durable session ID, emitted first), status (build phases like "Crate-digging…" or "Finalising the show…"), seed (for Surprise me builds), show (the finished show), and error (terminal failures).

The session ID goes out first so the browser can reconnect if the stream drops. A resume request sends { resumeSessionId } back to /api/show, which re-attaches to the durable session stream with ?startIndex=0. Eve replays every event from the beginning and then continues live, so the client catches up without losing progress; whether the show finished while the connection was down or is still building.

The build has a per-request timeout budget that scales with the requested track count: a base of 45 seconds plus 11 seconds per track, capped at 280 seconds (under the Hobby plan's 300-second maxDuration ceiling). If a user asks for 15 tracks, the budget reflects that; if the build times out on a large request, the error message suggests trying fewer tracks.

Authentication and rate limiting

The app uses Spotify's PKCE auth flow; there's no server-side client secret for the user session. The /api/show endpoint verifies the user's Spotify token by calling GET /me, which also gives us a stable user ID for rate limiting via checkRateLimit.

The more interesting part is how the user's Spotify token reaches the agent. When the listener has "exclude what I know" toggled on, /api/show forwards their token as an x-spotify-user-token header on the internal hop to /eve/v1/session. The Eve channel's auth walk picks it up: a withSpotifyToken decorator wraps the standard authenticators (localDev() and vercelOidc()) and lifts the header value into the session's durable auth attributes. From there, search_tracks reads it off ctx.session.auth to check Liked Songs and top tracks on demand. The token never enters the model's context, and it doesn't need an external store; it rides the session's own durable state.

Brief classification

Before spending an agent session on a build, /api/show runs the brief through a classifier that checks whether the request is actually about music. "Help me write Python" or "what's the weather" gets rejected with a 422 before any Eve session is created. The agent's own instructions also refuse off-topic requests, but the pre-flight classifier saves the cost of a wasted session.

Deployment protection

One thing that caught us out during productionisation: Vercel's Deployment Protection intercepts server-side requests between your own routes. The internal call from /api/show to /eve/v1/session was being intercepted and returning an HTML authentication page instead of JSON, which broke the agent session creation.

The fix was to disable Deployment Protection entirely; this is safe because /api/show is already gated via the Spotify token verification (no valid Spotify session, no show build) and the /eve/v1/* routes use Vercel OIDC. There's a cleaner path available by adding a custom domain, which would let us re-enable Standard Protection while keeping the internal self-call functional, since Standard Protection leaves custom production domains public.

What didn't work

Early on I tried splitting the agent into two tiers: a strong model for the creative crate-digging work and a cheaper subagent for resolving Spotify URIs. The thinking was that URI resolution is mechanical and doesn't need an expensive model. In practice this was structurally wrong; disambiguating which track to pick from a set of search results requires the same musical taste that drove the selection in the first place. A cheap model picking "the first result" produced worse shows than a single model making both decisions. I documented this in Linear rather than silently dropping it; it's a useful reminder that not every cost optimisation is a good one.

Guest access

The app supports guest show builds with a brief classifier that determines whether a brief is safe to process without full authentication. Guest users get rate-limited by IP rather than Spotify user ID, and don't get the full Spotify Connect playback or playlist saving features due to restrictions in Spotify Developer capability.

What's next

The eval suite and unit tests for the show graders exist but I won't be turning that into a CI pipeline; that feels unnecessary for this project. I'm tempted to add a YouTube version alongside which would make it more publicly testable but TBD...

The app is live and the source code is on GitHub. It is now themed to the rest of the site and you can try it if you clone and have a Spotify developer account.