Build
Token metadata
The on-chain tokenURI, the document behind it, and who can write it.
On-chain discovery#
Every launch token implements ERC-1046, so its profile is reachable from the chain alone. No indexer needed, no allowlist to get on:
tokenURI() returns (string)
supportsInterface(bytes4) returns (bool) // ERC-165 + ERC-1046 (0x3c130d90)The URI is built from a fixed base plus the token's own lowercased address:
https://api.joseon.fun/meta/0x171e21465bfd49270bef4754f99479d88470b926.jsonThat base is written once at deployment and has no setter. There is no owner and no admin function that can repoint a live token's metadata — not the creator's, not ours.
The document#
curl -s https://api.joseon.fun/meta/0x171e…b926.json{
"name": "Test",
"symbol": "TEST",
"description": "A test launch",
"image": "https://api.joseon.fun/media/0x171e…b926.jpg",
"external_url": "https://example.com",
"website": "https://example.com",
"twitter": "https://x.com/example",
"telegram": null,
"extensions": {
"joseon": {
"chainId": 91342,
"network": "GIWA Sepolia",
"token": "0x171e…b926",
"curve": "0xb90c…4520",
"metadataUri": "https://api.joseon.fun/api/tokens/0x171e…b926",
"ipfs": null,
"feeRecipient": "0x76ce…9da4",
"feeShares": [{ "wallet": "0x76ce…9da4", "bps": 10000 }],
"socials": { "website": "https://example.com", "twitter": "https://x.com/example", "telegram": null }
}
}
}The top level follows the name / symbol / description / image convention wallets and explorers already parse.
Joseon-specific fields live under extensions.joseon, so a generic consumer can ignore them entirely.
/api/tokens/:addr and /api/tokens/:addr/metadata return the same shape.
Writing it#
Only the launch's on-chain creator can write a token's profile, and they prove it with a signature. The flow is two calls: request a challenge, then submit the fields together with a signature over it. The signature covers the chain, the token, the signer, an expiry, and a digest of the exact fields being written, so it is good for that write and nothing else.
In the app this is just part of creating a token: crop a logo, fill in the fields, sign once. If you are doing it yourself, the important detail is that the digest must be computed over a canonical serialisation of the payload — the indexer recomputes it and compares, so any difference in key ordering fails verification.
Once accepted: the image is stored and served with an immutable cache header, the document is written to
/meta/:addr.json, and if IPFS pinning is configured the document is pinned and the ipfs field filled in.
Only the creator can write, and only with a signature. A token's profile is what the trading UI reads, so it has to be as trustworthy as the launch itself.
Storage#
| Layer | Holds | Mutable? |
|---|---|---|
| Token contract | the metadata base, and the URI derived from it | no |
Our object storage (/meta/, /media/) | the JSON document and the image | yes, by the creator |
| IPFS (optional) | a pinned copy | no, content-addressed |
The pointer is immutable, the content behind it is not. That is the honest tradeoff: it means a creator can fix a typo or replace a logo, and it means the document is not a permanent record unless it is pinned.
If you are building something that needs an immutable profile, prefer the ipfs field when it is present over the
hosted URL.
Fee shares (retired)#
feeShares was metadata only — on-chain accrual has always targeted the single fee recipient in the registry — so the
field was removed from the app on 2026-07-28 and new documents carry null. Tokens launched before then may still have
it; treat it as historical and non-binding. For a split that actually holds, point the fee recipient at a splitter
contract. See fees.
Legacy documents#
If a token predates the /meta/*.json documents, the first request rebuilds it from the indexer's row and caches it. So
the endpoint works for every indexed token regardless of when it launched.