Build
Token metadata
ERC-1046 tokenURI, R2 hosting, and optional IPFS pinning.
On-chain discovery#
A JoseonTokenV2 implements ERC-1046, so its off-chain 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 an immutable base plus the token's own lowercased address:
https://api.joseon.fun/meta/0xdd34da56701987bfb24b255c9f10bd9f4dff86ab.jsonmetadataBase is written once in the constructor 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.
V1 tokens (JoseonToken) expose only name and symbol, which is why the logo was undiscoverable on-chain and why V2
shipped as a new token type behind a new factory rather than as an upgrade.
The document#
curl -s https://api.joseon.fun/meta/0xdd34…86ab.json{
"name": "Giwa",
"symbol": "GIWA",
"description": "Test",
"image": "https://api.joseon.fun/media/0xdd34…86ab.jpg",
"external_url": "https://giwa.io",
"website": "https://giwa.io",
"twitter": null,
"telegram": null,
"extensions": {
"joseon": {
"chainId": 91342,
"network": "GIWA Sepolia",
"token": "0xdd34…86ab",
"curve": "0x03d1…0925",
"metadataUri": "https://api.joseon.fun/api/tokens/0xdd34…86ab",
"ipfs": null,
"feeRecipient": "0x76ce…9da4",
"feeShares": [{ "wallet": "0x76ce…9da4", "bps": 10000 }],
"socials": { "website": "https://giwa.io", "twitter": null, "telegram": null }
}
}
}The top level follows the usual name / symbol / description / image convention that wallets and explorers
already parse. Joseon-specific fields live under extensions.joseon so a generic consumer can ignore them.
/api/tokens/:addr and /api/tokens/:addr/metadata return the same shape.
Uploading#
curl -X POST https://api.joseon.fun/api/tokens/0xTOKEN/metadata \
-F image=@logo.png \
-F description='What this token is' \
-F website=https://example.com \
-F twitter=https://x.com/example \
-F telegram=https://t.me/exampleWhat happens: the image goes to R2 and is served from /media/… with an immutable cache header, the document is written
to /meta/:addr.json, and if a Pinata token is configured the whole thing is also pinned to IPFS and ipfs is
populated.
In the app this is part of the create flow — you crop a logo, fill in the fields, and it is uploaded once the token address exists.
Storage#
| Layer | Holds | Mutable? |
|---|---|---|
| Token contract | metadataBase, and the URI derived from it | no |
R2 (/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, use the ipfs field when present rather than the R2
URL.
Fee shares#
feeShares is metadata only. On-chain accrual always targets the single feeRecipient in the registry, so a
multi-wallet split is a display convention that the contracts do not enforce. For trustless splitting, point
feeRecipient at a splitter contract instead. See fees.
Legacy documents#
If a token was created before /meta/*.json was written, the first request rebuilds the document from the indexer's
row and caches it to R2. So the endpoint works for every indexed token regardless of when it launched.