Joseon docs
AppExplorerLaunch app

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.json

metadataBase 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/example

What 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#

LayerHoldsMutable?
Token contractmetadataBase, and the URI derived from itno
R2 (/meta/, /media/)the JSON document and the imageyes, by the creator
IPFS (optional)a pinned copyno, 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.