ENS-powered identity for pets. Mint a
*.dogid.ethor*.catid.ethsubname, host a pet profile on IPFS, and print a QR collar tag that helps strangers reunite you with your lost pet.
PetID is a Web3 take on the pet ID tag. Instead of an engraved phone number, your pet gets a permanent, on-chain identity (an ENS subname) and a tamper-proof profile page hosted on IPFS. A scannable QR code on the collar resolves to that profile in any browser — no app, no account, no central server.
This repository ships the fiat + crypto version of PetID, designed for Cloudflare Pages with a Railway worker. The live production app is the crypto-only IPFS build at petid-eth-ipfs, served from petid.eth. This repo is paused pending a new payment merchant for the fiat flow.
petid-eth (this repo) |
petid-eth-ipfs ✅ live |
|
|---|---|---|
| Status | Paused — awaiting new payment merchant | Live at petid.eth |
| Payment | Helio (card / crypto, $19.99) or on-chain ETH | On-chain ETH only |
| Hosting | Cloudflare Pages | IPFS → petid.eth |
| Backend | Supabase + Railway worker | None (fully client-side) |
| Pinata | Server-side SDK | Browser scoped JWT |
| Mint | Worker mints on behalf of user (fiat flow) | User mints directly from wallet |
| Audience | Mainstream pet owners | Crypto-native users |
Both apps mint into the same PetSubnameRegistrar contract on Ethereum mainnet, so a name minted from either flow is indistinguishable on-chain.
┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Wizard │──▶│ Payment │──▶│ IPFS upload │──▶│ ENS mint │
└──────────┘ └──────────┘ └──────────────┘ └──────────┘
│ │ │ │
pet details Helio or wallet photo + profile subname +
HTML (CIDv1) contenthash
- Owner fills in pet info, picks
dogid.ethorcatid.eth, chooses a subname. - Pays $19.99 via Helio (card or crypto) or 0.00825 ETH directly from their wallet.
- Photo and a generated profile HTML page are uploaded to IPFS via Pinata (CIDv1,
bafy…). - The
PetSubnameRegistrarcontract mints the subname underdogid.eth/catid.ethand sets the IPFScontenthashon the resolver. - The owner gets a printable QR collar tag pointing to
https://<subname>.dogid.eth.link.
If the pet is found, a stranger scans the QR. The profile resolves through any ENS gateway, no app or account required.
Frontend
- Next.js 15.5.2 (App Router, edge runtime) — pinned because
@cloudflare/next-on-pagescaps at 15.5.2 - React 19
- Tailwind CSS v4
- RainbowKit + wagmi v2 + viem (crypto-native flow)
- Handlebars (profile template rendering)
qrcode(QR generation)
Backend / infra
- Cloudflare Pages (
wrangler pages deploy) - Supabase Postgres (pet record + mint status)
- Pinata (IPFS pinning,
cidVersion: 1) - Helio (payments — replaces Stripe; supports card and crypto)
- Resend (transactional email)
- Railway Node.js worker (
worker/index.ts) — polls Supabase every 30s and runs the mint pipeline
Smart contracts
- Solidity 0.8.x, Hardhat, OpenZeppelin v5
contracts/src/PetSubnameRegistrar.sol— single multi-parent ENS subname registrar
Mainnet (current):
| Contract | Address |
|---|---|
PetSubnameRegistrar (v3) |
0xfd428E9188c9D858D48Ca2fEE9199Cc2d66D61C1 |
ENS NameWrapper |
0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401 |
dogid.eth and catid.eth are both wrapped via NameWrapper and setApprovalForAll is granted to the registrar so it can issue subnames under either parent.
Fuses: Subnames are minted with CANNOT_UNWRAP | PARENT_CANNOT_CONTROL (the latter is 1 << 16, not 1 << 17 — that's IS_DOT_ETH and the NameWrapper will reject it).
_mintSubname pattern: The contract temporarily sets address(this) as the subname owner so it can write the contenthash on the resolver, then safeTransferFroms the wrapped NFT to the actual owner. This is why PetSubnameRegistrar inherits ERC1155Holder — without it the transfer reverts.
Fee: 0.00825 ETH, set via setFee().
Contract history
| Version | Address | Status |
|---|---|---|
| v1 | 0xB67E50524560a73C56E55B3Ae33f94D6541841A0 |
Wrong fuse (1 << 17 = IS_DOT_ETH), rejected by NameWrapper |
| v2 | 0x3b410a5cE72d3984625a367c0608dFD72C0C6B1b |
Fixed fuse, but missing ERC1155Holder → transfer reverted |
| v3 | 0xfd428E9188c9D858D48Ca2fEE9199Cc2d66D61C1 |
Current. Both issues fixed, verified on Etherscan |
petid-eth/
├── app/ Next.js App Router
│ ├── page.tsx landing
│ ├── register/ wizard (fiat + crypto flows)
│ ├── api/
│ │ ├── helio-webhook/ PAYMENT_SUCCESS handler (HMAC-SHA256)
│ │ └── create-checkout/ creates Helio pay link
│ ├── [subdomain]/ profile page route
│ ├── admin/ ops dashboard
│ ├── privacy/, terms/, success/, found/
│ ├── layout.tsx, providers.tsx, globals.css
├── components/ shared UI
├── hooks/
│ └── useRegister.ts wagmi mint hook (writeContractAsync)
├── lib/ wagmi config, supabase client, IPFS helpers
├── contracts/
│ ├── src/PetSubnameRegistrar.sol
│ ├── scripts/, test/, deployments/
│ └── hardhat.config.cjs
├── worker/ Railway mint worker
│ └── index.ts photo → IPFS → HTML → IPFS → ENS → email
├── scripts/
│ ├── setup-registrar.mjs approve registrar on NameWrapper
│ └── wrap-names.mjs wrap dogid.eth / catid.eth
├── supabase/ schema + migrations
├── public/, types/
├── next.config.ts, wrangler.toml
└── package.json
# install
npm install
# dev server
npm run dev # http://localhost:3000
# build for Cloudflare Pages
npm run pages:build
# deploy to Cloudflare Pages
npm run deploy # builds + wrangler pages deployContracts (separate workspace under contracts/):
cd contracts
npm install
npm run compile
npm run deploy:sepolia # or deploy:mainnetWorker (separate workspace under worker/, Railway-deployed):
cd worker
npm install
npm run devThe worker is split into its own folder so Railway's Next.js CVE scan doesn't trip on the parent project.
Minimum set for the web app (.env.local):
# Public
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=9588ece98cf4d20f8b72ed925174849d
NEXT_PUBLIC_PETID_REGISTRAR_ADDRESS=0xfd428E9188c9D858D48Ca2fEE9199Cc2d66D61C1
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
# Server
SUPABASE_SERVICE_ROLE_KEY=
PINATA_JWT=
HELIO_API_KEY=
HELIO_WEBHOOK_SECRET=
RESEND_API_KEY=
Worker additionally needs MAINNET_RPC_URL and REGISTRAR_PRIVATE_KEY.
- CID version matters. All Pinata uploads must use
cidVersion: 1(bafy…). CIDv0 (Qm…) causes ENScontenthashresolution issues. - Contenthash encoding: base32-decode the CID then prepend
0xe301(IPFS multicodec). - Use
.link, not.limofor profile URLs. The QR codes encode.link. writeContractAsync, notwriteContract. The wizard runs async IPFS uploads between the user click and the wallet popup;writeContractloses the gesture chain and the popup never appears.- Next.js 15 + Cloudflare Pages: every dynamic route needs
export const runtime = 'edge'. - AGENTS.md in this repo warns that Next.js 15.5 has breaking changes from older training data — check
node_modules/next/dist/docs/if anything feels off.
- Live production:
petid.eth— served from IPFS, built frompetid-eth-ipfs - This repo (paused): Cloudflare Pages target —
https://main.petid-eth.pages.dev. The fiat flow is on hold until a new payment merchant is wired in to replace Helio. - Worker: Railway (root dir =
worker/) — not currently running - Repo:
RWA-ID/pet-id(private)
All rights reserved. Contact the maintainers for licensing inquiries.