Last active
October 2, 2025 02:42
-
-
Save tunnckoCore/1039a72e7a9682b785019ecd7bc4412c to your computer and use it in GitHub Desktop.
Fetcher for all 192 ethscribed 0xNekos OG - could output item by item, or a collection manifest
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
| import * as cheerio from "cheerio"; | |
| export type Attribute = { | |
| trait_type: string; | |
| value: string; | |
| }; | |
| export type CollectionItem = { | |
| id: string; | |
| index: number; | |
| sha: string; | |
| name: string; | |
| description: string; | |
| attributes: Attribute[]; | |
| }; | |
| export type CollectionMetadata = { | |
| name: string; | |
| logo_image: string | null; | |
| banner_image: string | null; | |
| total_supply: number; | |
| slug: string; | |
| description: string; | |
| website_url: string | null; | |
| twitter_url: string | null; | |
| discord_url: string | null; | |
| background_color: string | null; | |
| collection_items: CollectionItem[]; | |
| }; | |
| type EthscriptionItem = PrettifyRecursive<{ | |
| block_number: number; | |
| block_hash: string; | |
| block_timestamp: number; | |
| transaction_hash: string; | |
| transaction_index: number; | |
| transaction_fee: number; | |
| creator: string; | |
| receiver: string; | |
| content_sha: string; | |
| current_owner: string; | |
| previous_owner: string; | |
| ethscription_number: number; | |
| content_uri: string; | |
| }>; | |
| type PrettifyRecursive<T> = { | |
| [K in keyof T]: T[K] extends object | |
| ? T[K] extends infer O | |
| ? O extends Date | RegExp | Function | |
| ? O | |
| : PrettifyRecursive<O> | |
| : never | |
| : T[K]; | |
| } & {}; | |
| type EthscriptionsResponse = PrettifyRecursive<{ | |
| result: EthscriptionItem[]; | |
| pagination: { | |
| has_more: boolean; | |
| page_key?: string; | |
| }; | |
| }>; | |
| type Prettify<T> = { | |
| [K in keyof T]: T[K]; | |
| }; | |
| type Traits = { | |
| background: string; | |
| cat: string; | |
| eyes: string; | |
| cursor: string; | |
| }; | |
| type ItemWithMetadata = EthscriptionsResponse["result"][0] & { | |
| traits: Traits; | |
| }; | |
| type FinalItems = PrettifyRecursive<{ | |
| item: ItemWithMetadata & { | |
| attributes: ERC721MetadataAttributes[]; | |
| }; | |
| neko: Omit<ItemWithMetadata, "content_uri">; | |
| }>[]; | |
| type ERC721MetadataAttributes = { | |
| trait_type: string; | |
| value: string; | |
| }; | |
| const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); | |
| async function fetchAllEthscriptions(): Promise<FinalItems> { | |
| const baseUrl = "https://mainnet.api.calldata.space/ethscriptions"; | |
| const baseParams = new URLSearchParams({ | |
| reverse: "true", | |
| creator: "0x9d9db340778139774cf73dfb7bf27498fa67978f", | |
| content_type: "text%2Fhtml", | |
| per_page: "100", | |
| with: "content_uri,current_owner,previous_owner,ethscription_number", | |
| only: "block_number,block_timestamp,block_hash,transaction_hash,transaction_index,transaction_fee,ethscription_number,creator,receiver,content_sha", | |
| }); | |
| const allItems: FinalItems = []; | |
| let hasMore = true; | |
| let pageKey: string | undefined; | |
| while (hasMore) { | |
| // Build URL with current parameters | |
| const params = new URLSearchParams(baseParams); | |
| if (pageKey) { | |
| params.append("page_key", pageKey); | |
| } | |
| const url = `${baseUrl}?${params.toString()}`; | |
| try { | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data: EthscriptionsResponse = await response.json(); | |
| const nekos: FinalItems = await Promise.all( | |
| data.result.map(async (item) => { | |
| item.ethscription_number = Number(item.ethscription_number); | |
| const response = await fetch(item.content_uri); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const html = await response.text(); | |
| const $ = cheerio.load(html); | |
| const attributes: ERC721MetadataAttributes[] = []; | |
| let traits: Traits = {} as unknown as Traits; | |
| $(".token-traits .tag").each((_, element) => { | |
| const $tag = $(element); | |
| const key = $tag.find(".trait-key").text().trim().toLowerCase(); | |
| const value = $tag.find(".trait-value").text().trim(); | |
| // Skip if it's the name tag or if key/value is empty | |
| if (key && value && key !== "license") { | |
| attributes.push({ | |
| trait_type: key, | |
| value: value, | |
| }); | |
| traits = traits || {}; | |
| traits[key] = value; | |
| } | |
| }); | |
| const { content_uri, ...neko } = { ...item, traits }; | |
| return { item: { ...item, traits, attributes }, neko }; | |
| }), | |
| ); | |
| // console.log(nekos.map((x) => x.neko)); | |
| allItems.push(...nekos); | |
| // Update pagination state | |
| hasMore = data.pagination.has_more; | |
| pageKey = data.pagination.page_key; | |
| // Optional: Add a small delay to be respectful to the API | |
| // if (hasMore) { | |
| // await new Promise(resolve => setTimeout(resolve, 100)); | |
| // } | |
| } catch (error) { | |
| console.error("Error fetching ethscriptions:", error); | |
| throw error; | |
| } | |
| } | |
| // console.log(`Finished! Total items fetched: ${allItems.length}`); | |
| return allItems; | |
| } | |
| // Example usage | |
| async function main() { | |
| try { | |
| const allEthscriptions = await fetchAllEthscriptions(); | |
| // x.item = includes content_uri | |
| // console.log( | |
| // JSON.stringify( | |
| // allEthscriptions.map((x) => x.neko), | |
| // null, | |
| // 2, | |
| // ), | |
| // ); | |
| // let i = 1; | |
| // for await (const { neko } of allEthscriptions) { | |
| // await Bun.write(`./public/metadata/${i}.json`, JSON.stringify(neko)); | |
| // i++; | |
| // } | |
| // collection metadata format | |
| // https://github.com/Ethereum-Phunks/curated-metadata | |
| const collectionMetadata: CollectionMetadata = { | |
| total_supply: 192, | |
| name: "0xNekos OG", | |
| slug: "0xnekos-og", | |
| logo_image: | |
| "https://api.ethscriptions.com/v2/ethscriptions/0x7c19b69abdf38cebc6de9a955f4f2887d1815e508098bf4fb347f8d8bfc1834e/data", | |
| banner_image: null, | |
| description: | |
| "Generative art collection of fresh, new, unique and optimized 192 0xNeko Cats as Ethscriptions. Inspired by 1989 game, they were originally 100 free minted as Ethereum NFTs in 2021. Later, the same exact 100 were free minted on Bitcoin Ordinals in April 2023.", | |
| background_color: null, | |
| website_url: null, | |
| discord_url: null, | |
| twitter_url: "https://twitter.com/wgw_eth", | |
| collection_items: allEthscriptions.map((x, index) => { | |
| const item: CollectionItem = { | |
| id: x.item.transaction_hash, | |
| index: index + 1, | |
| sha: x.item.content_sha.replace("0x", ""), | |
| name: `0xNeko OG #${index + 1}`, | |
| description: "", | |
| attributes: x.item.attributes, | |
| }; | |
| return item; | |
| }), | |
| }; | |
| console.log(JSON.stringify(collectionMetadata, null, 2)); | |
| } catch (error) { | |
| console.error("Failed to fetch ethscriptions:", error); | |
| } | |
| } | |
| main(); | |
| export { | |
| fetchAllEthscriptions, | |
| type EthscriptionItem, | |
| type EthscriptionsResponse, | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment