{"_id":"@1001-digital/ponder-artifacts","_rev":"2-3e9a8c6e06fc4ec6c65b397c8373c394","name":"@1001-digital/ponder-artifacts","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@1001-digital/ponder-artifacts","version":"0.1.0","license":"MIT","_id":"@1001-digital/ponder-artifacts@0.1.0","maintainers":[{"name":"jwahdatehagh","email":"jalil@1001.digital"}],"dist":{"shasum":"1b6a638c4bc4b15f0253aa72e4aa1105cbe6a17c","tarball":"https://registry.npmjs.org/@1001-digital/ponder-artifacts/-/ponder-artifacts-0.1.0.tgz","fileCount":4,"integrity":"sha512-DoSa/iA5Dh7qetuW541qaqoGSfaCzmLGWDSqqOkCgDr2i9HkiWFP+aFxWmSlkedCQPD5tjn3rxEdegpCt7mfdg==","signatures":[{"sig":"MEUCIGm0z0WdOGoFN5Tc+Xz/OI6M22gOzCMraoORONNzpe+HAiEAyHPhxjzf5CaZ7RHr3s3JUUg5wWpUZb51GvU6Ep2CaTI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":28270},"type":"module","_from":"file:1001-digital-ponder-artifacts-0.1.0.tgz","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"tsup src/index.ts --format esm --dts --watch","build":"tsup src/index.ts --format esm --dts","typecheck":"tsc --noEmit"},"_npmUser":{"name":"jwahdatehagh","email":"jalil@1001.digital"},"_resolved":"/tmp/dbd7f0037129e62726004d1edbe68472/1001-digital-ponder-artifacts-0.1.0.tgz","_integrity":"sha512-DoSa/iA5Dh7qetuW541qaqoGSfaCzmLGWDSqqOkCgDr2i9HkiWFP+aFxWmSlkedCQPD5tjn3rxEdegpCt7mfdg==","_npmVersion":"11.6.1","directories":{},"_nodeVersion":"24.11.0","_hasShrinkwrap":false,"devDependencies":{"pg":"^8.13.0","hono":"^4.5.0","tsup":"^8.0.0","viem":"^2.21.0","@types/pg":"^8.11.0","typescript":"^5.2.0","drizzle-orm":"^0.38.0","@electric-sql/pglite":"^0.2.13"},"peerDependencies":{"pg":">=8.0.0","hono":">=4.0.0","viem":">=2.0.0","drizzle-orm":">=0.38.0","@electric-sql/pglite":">=0.2.0"},"peerDependenciesMeta":{"pg":{"optional":true},"@electric-sql/pglite":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/ponder-artifacts_0.1.0_1771895813293_0.4190039043657514","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@1001-digital/ponder-artifacts","version":"0.1.1","license":"MIT","type":"module","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"peerDependencies":{"drizzle-orm":">=0.38.0","hono":">=4.0.0","viem":">=2.0.0","pg":">=8.0.0","@electric-sql/pglite":">=0.2.0"},"peerDependenciesMeta":{"pg":{"optional":true},"@electric-sql/pglite":{"optional":true}},"devDependencies":{"@electric-sql/pglite":"^0.2.13","@types/pg":"^8.11.0","drizzle-orm":"^0.38.0","hono":"^4.5.0","pg":"^8.13.0","tsup":"^8.0.0","typescript":"^5.2.0","viem":"^2.21.0"},"scripts":{"build":"tsup src/index.ts --format esm --dts","dev":"tsup src/index.ts --format esm --dts --watch","typecheck":"tsc --noEmit"},"_id":"@1001-digital/ponder-artifacts@0.1.1","description":"Server-side artifact metadata caching for [Ponder](https://ponder.sh) indexers. Resolves NFT token and collection metadata (ERC721 + ERC1155) on demand, caches it with a 30-day TTL, and serves it via ready-to-mount Hono API routes.","_integrity":"sha512-vu/eamj0oMJyLU0yaBPrfMYHWNizRxah+h1jRYQv8cAffbM+OnNb1PalO1I5UZGYwzcpiW/eZDsVnWQ3dfXgSw==","_resolved":"/tmp/7e7a1e6f0febb58d6154998b86f7f5b9/1001-digital-ponder-artifacts-0.1.1.tgz","_from":"file:1001-digital-ponder-artifacts-0.1.1.tgz","_nodeVersion":"24.11.0","_npmVersion":"11.6.1","dist":{"integrity":"sha512-vu/eamj0oMJyLU0yaBPrfMYHWNizRxah+h1jRYQv8cAffbM+OnNb1PalO1I5UZGYwzcpiW/eZDsVnWQ3dfXgSw==","shasum":"87dd78680f87aa23c72a6b38c14e033ca2ac589a","tarball":"https://registry.npmjs.org/@1001-digital/ponder-artifacts/-/ponder-artifacts-0.1.1.tgz","fileCount":5,"unpackedSize":35254,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFDAsDpuC6L/9gLwHQBJsr6aPckoLtg1+y5b6wZcQAtjAiBESZtzCe8QkC7f+DMJgcJzf7PcivVp+DvDxLnLyLhUwQ=="}]},"_npmUser":{"name":"jwahdatehagh","email":"jalil@1001.digital"},"directories":{},"maintainers":[{"name":"jwahdatehagh","email":"jalil@1001.digital"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ponder-artifacts_0.1.1_1771896796087_0.5913713806511087"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-24T01:16:53.117Z","modified":"2026-02-24T01:33:16.390Z","0.1.0":"2026-02-24T01:16:53.421Z","0.1.1":"2026-02-24T01:33:16.266Z"},"license":"MIT","maintainers":[{"name":"jwahdatehagh","email":"jalil@1001.digital"}],"readme":"# @1001-digital/ponder-artifacts\n\nServer-side artifact metadata caching for [Ponder](https://ponder.sh) indexers. Resolves NFT token and collection metadata (ERC721 + ERC1155) on demand, caches it with a 30-day TTL, and serves it via ready-to-mount Hono API routes.\n\nWorks with both **PostgreSQL** and **PGlite** (Ponder's default embedded database) — no Postgres required for development.\n\n## Why offchain?\n\nPonder rebuilds its onchain tables from scratch on every reindex. Token metadata (names, images, descriptions) doesn't change often, but fetching it from IPFS/Arweave on every page load is slow and redundant across clients.\n\nThis package stores artifact metadata in a **separate offchain table** that persists across reindexes. Metadata is resolved lazily on first request and cached with a 30-day TTL. Many concurrent requests for the same token result in a single RPC + IPFS lookup — not one per client.\n\n## Install\n\n```bash\npnpm add @1001-digital/ponder-artifacts\n```\n\nPeer dependencies (your ponder app should already have these):\n\n```bash\npnpm add drizzle-orm hono viem\n```\n\n## Quick start\n\n### 1. Mount the routes\n\n```typescript\n// src/api/index.ts\nimport { db, publicClients } from \"ponder:api\";\nimport schema from \"ponder:schema\";\nimport { Hono } from \"hono\";\nimport { client, graphql } from \"ponder\";\nimport {\n  createArtifactRoutes,\n  createOffchainDb,\n} from \"@1001-digital/ponder-artifacts\";\n\nconst { db: artifactDb } = await createOffchainDb();\n\nconst app = new Hono();\n\napp.route(\n  \"/artifacts\",\n  createArtifactRoutes({\n    client: publicClients[\"ethereum\"],\n    db: artifactDb,\n  }),\n);\n\napp.use(\"/sql/*\", client({ db, schema }));\napp.use(\"/\", graphql({ db, schema }));\n\nexport default app;\n```\n\nThat's it. You now have:\n\n| Method | Path | Behavior |\n|--------|------|----------|\n| GET | `/artifacts/:address` | Returns cached collection info, refreshes if stale (>30 days) |\n| POST | `/artifacts/:address` | Force refresh collection from chain |\n| GET | `/artifacts/:collection/:tokenId` | Returns cached token metadata, refreshes if stale |\n| POST | `/artifacts/:collection/:tokenId` | Force refresh token from chain |\n\nOn fetch failure, stale cache is returned if available; 500 otherwise.\n\n## How `createOffchainDb` works\n\n`createOffchainDb()` auto-detects your database setup:\n\n- **With `DATABASE_URL`** (or `DATABASE_PRIVATE_URL`): connects to PostgreSQL, creates the `offchain` schema and tables if they don't exist.\n- **Without `DATABASE_URL`**: uses PGlite (Postgres-in-WASM), stores data in `.ponder/artifacts/` by default.\n\n```typescript\n// Auto-detect (recommended)\nconst { db } = await createOffchainDb();\n\n// Explicit Postgres\nconst { db } = await createOffchainDb({ databaseUrl: \"postgresql://...\" });\n\n// Explicit PGlite with custom directory\nconst { db } = await createOffchainDb({ dataDir: \".data/artifacts\" });\n```\n\n## Using the service directly\n\nFor metadata resolution outside of API routes (e.g. in scripts), use `createArtifactService`:\n\n```typescript\nimport {\n  createArtifactService,\n  createOffchainDb,\n} from \"@1001-digital/ponder-artifacts\";\nimport { createPublicClient, http } from \"viem\";\nimport { mainnet } from \"viem/chains\";\n\nconst client = createPublicClient({ chain: mainnet, transport: http() });\nconst { db } = await createOffchainDb();\n\nconst artifacts = createArtifactService({ client, db });\n\n// Fetch cached token (returns null if not yet cached)\nconst token = await artifacts.fetchToken(\"0xbc4c...\", \"42\");\n\n// Update token metadata from chain + IPFS\nawait artifacts.updateToken(\"0xbc4c...\", \"42\");\n\n// Fetch cached collection\nconst collection = await artifacts.fetchCollection(\"0xbc4c...\");\n\n// Update collection metadata from chain\nawait artifacts.updateCollection(\"0xbc4c...\");\n\n// Check if a cached timestamp is still fresh\nartifacts.isFresh(token?.updatedAt ?? null);\n```\n\n## Bring your own database\n\nIf you manage your own offchain database, skip `createOffchainDb` and pass your drizzle instance directly:\n\n```typescript\nimport { createArtifactRoutes } from \"@1001-digital/ponder-artifacts\";\nimport { getOffchainDb } from \"./services/database\";\n\napp.route(\n  \"/artifacts\",\n  createArtifactRoutes({\n    client: publicClients[\"ethereum\"],\n    db: getOffchainDb(),\n  }),\n);\n```\n\nThe package exports the schema for your drizzle config:\n\n```typescript\n// offchain.schema.ts\nexport { artifactToken, artifactCollection } from \"@1001-digital/ponder-artifacts\";\n```\n\n## Token standard detection\n\nThe service detects ERC721 vs ERC1155 via ERC165 `supportsInterface`:\n\n- **ERC721** (`0x80ac58cd`): calls `tokenURI(tokenId)`\n- **ERC1155** (`0xd9b67a26`): calls `uri(tokenId)`, replaces `{id}` with zero-padded hex token ID\n- **Unknown**: falls back to ERC721 `tokenURI`\n\n## URI resolution\n\nToken and collection URIs are resolved inline (no external dependency):\n\n- `ipfs://...` — resolved via IPFS gateway (default: `https://ipfs.io/ipfs/`)\n- `ar://...` — resolved via Arweave gateway (default: `https://arweave.net/`)\n- `Qm...` / `baf...` — treated as raw IPFS hashes\n- `data:application/json;base64,...` — decoded inline\n- `data:application/json,...` — URL-decoded inline\n- Everything else — fetched as-is\n\nCustom gateways can be passed via config:\n\n```typescript\ncreateArtifactRoutes({\n  client: publicClients[\"ethereum\"],\n  db: artifactDb,\n  ipfsGateway: \"https://my-gateway.io/ipfs/\",\n  arweaveGateway: \"https://my-arweave.io/\",\n});\n```\n\n## Configuration\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `client` | viem `PublicClient` | Client for on-chain reads (ERC165, tokenURI, contractURI, etc.) |\n| `db` | drizzle instance | For reading and writing metadata. Use `createOffchainDb()` or bring your own. |\n| `cacheTtl` | `number` (ms) | Cache freshness window. Defaults to 30 days. |\n| `ipfsGateway` | `string` | IPFS gateway URL. Defaults to `https://ipfs.io/ipfs/`. |\n| `arweaveGateway` | `string` | Arweave gateway URL. Defaults to `https://arweave.net/`. |\n\n## Data structures\n\n### Token\n\n```typescript\n{\n  collection: string;      // Lowercase hex address\n  tokenId: string;         // String representation of bigint\n  tokenStandard: string;   // \"erc721\" | \"erc1155\" | \"unknown\"\n  tokenUri: string | null; // Raw URI from contract\n  name: string | null;\n  description: string | null;\n  image: string | null;\n  animationUrl: string | null;\n  data: object | null;     // Full raw metadata blob\n  updatedAt: number;       // Unix timestamp (seconds)\n}\n```\n\n### Collection\n\n```typescript\n{\n  collection: string;       // Lowercase hex address\n  tokenStandard: string;    // \"erc721\" | \"erc1155\" | \"unknown\"\n  name: string | null;      // From on-chain name() or contractURI\n  symbol: string | null;    // From on-chain symbol() or contractURI\n  owner: string | null;     // From on-chain owner()\n  contractUri: string | null;\n  description: string | null;\n  image: string | null;\n  data: object | null;      // Full raw contractURI metadata blob\n  updatedAt: number;        // Unix timestamp (seconds)\n}\n```\n","readmeFilename":"README.md","description":"Server-side artifact metadata caching for [Ponder](https://ponder.sh) indexers. Resolves NFT token and collection metadata (ERC721 + ERC1155) on demand, caches it with a 30-day TTL, and serves it via ready-to-mount Hono API routes."}