feat(solana-relay): host launched coin images and metadata on Audius - #90
Open
dylanjeffers wants to merge 4 commits into
Open
dylanjeffers wants to merge 4 commits into
dylanjeffers wants to merge 4 commits into
Conversation
Coin logos went to Irys/Arweave and the Metaplex metadata JSON went with them, which meant a permanent Arweave dependency, a SOL top-up loop per fee payer, and metadata frozen at mint. Both now point at Audius: - the logo uploads to a content node (mediorum accepts img_square uploads unsigned, and stores them synchronously, so there's nothing to poll for) - the on-chain `uri` points at GET /v1/coins/:mint/metadata on the Audius API, which builds the document from the artist_coins row Drops the Irys uploader, the umi setup that only existed to drive it, and the 5-minute balance top-up interval. Existing coins keep their Arweave on-chain metadata - that's immutable. Their artist_coins.logo_uri is re-hosted separately by a backfill script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dylanjeffers
added a commit
to AudiusProject/api
that referenced
this pull request
Sep 9, 2026
…#1030) Part of the Irys → Audius coin migration ([#eng thread](https://audius-internal.slack.com/archives/CA80RCL77/p1787591999611799)). Coins launched through the launchpad upload their logo **and** their Metaplex off-chain metadata JSON to Irys/Arweave. We're moving both to Audius. Mediorum can host the image, but it only accepts `audio` / `img_square` / `img_backdrop` templates, so there's nowhere to put a JSON blob — this endpoint is that home. ## What this adds `GET /v1/coins/:mint/metadata` returns the Metaplex off-chain document built from the `artist_coins` row: ```json { "name": "Bear Coin", "symbol": "BEAR", "description": "$BEAR is an artist coin created by @bearartist on Audius. Learn more at https://audius.co/coins/BEAR", "image": "https://creatornode.audius.co/content/<cid>", "external_url": "https://audius.co/coins/BEAR", "attributes": [] } ``` Served **unwrapped** — no `data` envelope — because wallets and explorers read it directly as the standard document. There's a test asserting that specifically, since our convention everywhere else would break it. Because it's generated per request rather than frozen at mint, name/description/image become editable after launch for new coins. That's a bonus, not the goal. ## The description fallback The launchpad passes a templated description to the relay for on-chain metadata but deliberately never persists it — `LAUNCHPAD_COIN_DESCRIPTION` in the web client regenerates the same sentence, and storing it would make the Fan Club page cite itself. So reading `artist_coins.description` alone would have shipped an **empty description to Phantom, Solscan and Jupiter** for every launchpad coin. This regenerates the same sentence from the owner's handle when the column is empty. It's duplicated logic against the web client and I've noted that on both sides. ## Ordering This needs to be live before AudiusProject/pedalboard#90 rolls, since that PR starts baking this URL into on-chain metadata that can never be changed. ## Testing `TestV1CoinMetadata` covers the document shape, the absent envelope, the description fallback, empty-string serialization for nullable columns, and 404 on an unknown mint. Full `TestV1Coin*` suite passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…lting Both defaulted to docker-internal hosts. Prod sets neither, so launches would either fail the image upload or bake an unreachable metadata uri into the immutable DBC pool. Make them required so the relay fails at startup, and set test values in vitest.config.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The returned URL used whichever node accepted the upload, so a logo broke if that node went away even though the CID is replicated. Build it from contentGatewayUrl (audius_content_gateway_url, defaulting to the first content node) and drop the fallback to an arbitrary results entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Irys → Audius coin migration (#eng thread). This is the piece that changes what new coins point at.
Before
launch_coinuploaded the logo to Irys/Arweave and the Metaplex metadata JSON alongside it. That meant a permanent Arweave dependency, a umi/Irys uploader per fee payer, a 5-minute SOL top-up loop, and metadata frozen at mint.After
POST /uploadstakesimg_squarewith no signature, and stores image uploads synchronously — the response already carries the CID withstatus: done, so unlike audio there's nothing to poll for.GET /v1/coins/:mint/metadata, built from theartist_coinsrow. Mediorum has no JSON template, so it couldn't be content-addressed without a go-openaudio release and a 24h fleet roll.Drops
@metaplex-foundation/umi,umi-bundle-defaultsandumi-uploader-irys, the umi setup that existed only to drive Irys, and the top-up interval.Existing coins
Untouched here. Their on-chain metadata stays on Arweave — it's immutable, which is the caveat Ray signed off on in the thread ("we basically do that for description anyway"). Their
artist_coins.logo_uriis re-hosted by a script inartist-coins-scriptsso the product renders from Audius.Two things to look at
The metadata URL has a 404 window.
artist_coinsis written by the client'screateCoinafterconfirmLaunchCoin, so the URL doesn't resolve until a few seconds after the pool lands. Worse, that call sits in a non-blockingtry/catch— if it fails, the URL 404s forever. That failure already made a coin invisible in our product so it isn't a new failure mode, but it now also breaks wallet metadata. Probably worth retryingcreateCoinor moving it ahead of the pool tx, as a follow-up.Lockfile is edited by hand. Running
npm installrewrote all 50k lines (this repo is onlockfileVersion: 2, local npm is 11 and emits v3), so I removed just the three dependency entries from theapps/solana-relayworkspace node. The transitivenode_modules/@metaplex-foundation/*entries are still in the lock — harmless install bloat that a normalnpm installon the team's npm version will prune.Ordering
Needs AudiusProject/api#1030 live and AudiusProject/audius-k8s#519 applied before this image rolls. Without the k8s change the relay falls back to dev defaults and would bake an unroutable URL into permanent on-chain data.
Testing
5 tests on the upload helper: CID extraction, the
results-map fallback, failover to the next host, upload-level errors, and all-hosts-failed. Typecheck and lint clean.🤖 Generated with Claude Code