Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Injected by the Neon integration on Vercel. Do not commit real values.
DATABASE_URL=
DATABASE_URL_UNPOOLED=
# Optional. Defaults to https://testnet-rpc.boing.network
RPC_URL=https://testnet-rpc.boing.network
# Optional. When empty, writes are accepted only from the site's own origin.
APP_ORIGIN=
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
node_modules/
dist/
api/
.wrangler/
.data/
.dev.vars
.env
.env.*
!.env.example
.vercel/
*.log
coverage/
coverage/
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**A little bounce. A buddy for life.** A small, Boing-native virtual pet game: hatch a creature, say hello, feed it, and meet everyone on the public pasture.

Testnet only (6913). This is an MVP, not an NFT marketplace, financial product, or airdrop promise. No contracts, consensus changes, custody keys, or scheduled jobs. Nothing is deployed automatically.
Testnet only (6913). This is an MVP, not an NFT marketplace, financial product, or airdrop promise. No contracts, consensus changes, custody keys, or scheduled jobs. The public site is https://boing-pets.vercel.app. Pushing does not publish a new Vercel deployment until the GitHub project is connected.

## Run locally

Expand Down Expand Up @@ -33,6 +33,7 @@ Open **http://localhost:5177** (use this exact origin, not 127.0.0.1). Vite prox
4. **Daily hello** signs a server-generated, origin/account/chain/action/nonce/expiry-bound message. The server verifies Ed25519 over BLAKE3(UTF-8 message), matching current Express. No transaction fee.
5. **Feed** approves another tiny self-transfer and restores the belly. Feeding is capped at once per UTC day to prevent accidental repeat fees. Each action type is once/day; either care action counts for the streak, never twice on the same day.
6. The public pasture shows buddies, streaks, belly, joy and status. Yours is highlighted. Browse without connecting. Use Refresh to recompute time-dependent state.
7. The shared yard is a 3D view of those buddies. The first player can deploy the Boing reference NFT kennel, then each wallet mints one pet NFT. Staking checks that the wallet still holds that NFT. Bonding 0.01 testnet BOING is a real protocol bond. Kibble accrues from care, buildings, the staked NFT, and the bond, and is spent on a pen, snack stand, or toy cart.

Belly loses 25 points/day since hatch or last feed; joy loses 20/day since any care. After two days without feeding, a buddy is scruffy; after four, a stray. A hello cheers up a stray but only a feed restores its belly. No permanent death or deletion. Streaks count consecutive UTC calendar days; yesterday's streak remains visible until today is missed. This is a calendar-day ritual, not a rolling 24-hour timer.

Expand All @@ -42,9 +43,9 @@ Belly loses 25 points/day since hatch or last feed; joy loses 20/day since any c
- **Wallet:** published `@boing-network/boing-sdk` **0.5.40**, native `boing_requestAccounts`, `boing_sendTransaction`, `boing_signMessage`; 32-byte Ed25519 account ids, not EVM addresses. Extension account/chain changes disconnect; chain is checked before actions. The web fallback is pinned to testnet.
- **On-chain hatch/feed:** Boing's native Transfer payload has **no memo field**. Rather than invent an unsupported memo or deploy a new program, v1 uses an exact challenge-specific self-transfer of approximately **0.001 BOING** (less than 0.001001). The amount encodes a random per-action marker; Express displays the human-readable intent, which is **UI-only, not a signed memo**. Principal stays in the same wallet; only the network fee is spent. Users still need balance for amount plus fee. There is no treasury/sink recipient and no app fee.
- **Verification:** Worker checks the RPC's chain id, successful receipt, post-challenge block height, exact block receipt/id/index, transaction sender, self-recipient, and exact challenge amount. It does not trust a browser-supplied success flag or arbitrary transaction hash. Failed/unknown transactions never grant care. Pending ids are saved locally so users can retry confirmation without resending. Challenges expire after one hour; confirm within that window. If a transaction failed or its challenge expired, Forget pending record clears only the local recovery record (it does not cancel or refund any transaction).
- **Public state:** one small Worker + D1 with unique wallet, transaction, challenge, and `(owner, action, UTC day)` constraints. D1 batches atomically write hatch and its event. Event-sourced state avoids lost-update races between feed/check-in. Repeated confirmation is idempotent; a hash cannot hatch/feed twice.
- **Public state:** unique wallet, transaction, challenge, and `(owner, action, UTC day)` constraints. Hatch and its event commit in one transaction. Event-sourced state avoids lost-update races between feed/check-in. Repeated confirmation is idempotent; a hash cannot hatch/feed twice. Local development still uses the Worker and D1. Vercel uses Neon Postgres.
- **No cron:** status/decay/streaks derive lazily from timestamps on reads. No added Cloudflare trigger. Unconfirmed expired challenges are cleaned on challenge issuance. Confirmed challenge records remain as an audit trail. Gallery is paginated (24 pets/page).
- **Trust model:** Pets is a centralized off-chain care/index service backed by genuine testnet transactions, **not** a fully on-chain pet contract or transferable NFT. The pasture consists of submitted, verified claims; it is not an autonomous chain-wide scanner. No API means no pasture/care writes. The RPC is trusted for canonical inclusion; a malicious RPC or database operator is outside the v1 trust boundary. Event history is local until an operator provisions a shared D1 deployment. No anti-Sybil guarantee; per-wallet challenge throttling is not a substitute for public edge rate limiting.
- **Trust model:** Pets is a centralized off-chain care/index service backed by genuine testnet transactions, **not** a fully on-chain pet contract or transferable NFT. The pasture consists of submitted, verified claims; it is not an autonomous chain-wide scanner. No API means no pasture/care writes. The RPC is trusted for canonical inclusion; a malicious RPC or database operator is outside the v1 trust boundary. The Vercel app stores one shared pasture in Neon Postgres. A local Worker checkout keeps its own D1 file. No anti-Sybil guarantee; per-wallet challenge throttling is not a substitute for public edge rate limiting.

## API

Expand All @@ -53,13 +54,15 @@ Belly loses 25 points/day since hatch or last feed; joy loses 20/day since any c
- `POST /api/challenge` `{owner,action}` → one-hour challenge, exact signature message or native transaction request.
- `POST /api/confirm` `{id,signature}` for check-in, or `{id,tx_hash}` for hatch/feed.

Writes require the configured `APP_ORIGIN` and JSON bodies (4 KiB maximum), signatures or chain proofs. No private keys, sessions, cookies, client secrets, or wallet seed imports. Challenges are not authentication by themselves. Before public deployment, add edge/IP abuse controls (including for reads and wallet-rotation attacks), audit economics/security, test retention/backfill behavior, and monitor D1 read usage.
Writes require the page's own origin and JSON bodies (4 KiB maximum), signatures or chain proofs. No private keys, sessions, cookies, client secrets, or wallet seed imports. Visitors connect a Boing wallet; the app does not use Cursor sign-in. Challenges are not authentication by themselves. Before a widely shared launch, add edge/IP abuse controls (including for reads and wallet-rotation attacks), audit economics/security, and test retention/backfill behavior.

## Deployment is deliberately not set up
## Vercel

`wrangler.jsonc` is a local-first, unused-until-approved Worker + static assets configuration, with a **placeholder D1 id**, no routes, `workers_dev: false`, preview URLs disabled, and **no cron**. CI only bundles with `--dry-run`; it does not deploy or require secrets. Never deploy the placeholder config.
The public app is a Vite client plus one Node.js function on Vercel Fluid Compute. `api/[...path].ts` serves the same care API. The pasture is Neon Postgres in `iad1`, provisioned by the Vercel Marketplace integration and reached with `@neondatabase/serverless`. `DATABASE_URL_UNPOOLED` is preferred so the driver is not pooled twice. The function migrates on startup. Hunger, joy, and streaks are still derived on read. There is no cron.

When Nico approves a shared preview: create a dedicated D1 database, put its non-secret id in the config, set `APP_ORIGIN` to that preview's exact origin, run migrations on that database, configure static assets and appropriate limits, then separately approve deployment. Production/custom domain/mainnet are not configured. Only change `RPC_URL` to a trusted **testnet 6913** endpoint; the API rejects another chain.
`RPC_URL` defaults to `https://testnet-rpc.boing.network` (chain 6913). Leave `APP_ORIGIN` unset so each deployment origin is taken from the request. Visitors connect a Boing wallet. The project is `nico-builds/boing-pets`.

`wrangler.jsonc` remains a local-only Worker configuration: placeholder D1 id, no routes, `workers_dev: false`, preview URLs disabled, and no cron. CI dry-runs that bundle and does not deploy it. Never deploy the placeholder config. To run the Node server on this machine without Neon: `PETS_DB=sqlite npm start` after `npm run build`.

## Planned Passport quests (documentation only)

Expand All @@ -70,12 +73,12 @@ No changes to boing.network Passport or airdrop eligibility are made here. A fut

## Deferred

DEX-swap forage on boing.finance; live finance-indexer integration; Passport wiring; mainnet; naming/trading/transferring pets; NFTs or a dedicated pet VM program; permanent chain-wide indexing/backfill; production hardening/rate limiting; custom domain and deployment. Pet naming is intentionally not user-generated yet, avoiding moderation and HTML injection concerns.
DEX-swap forage on boing.finance; live finance-indexer integration; Passport wiring; mainnet; naming/trading/transferring pets; NFTs or a dedicated pet VM program; permanent chain-wide indexing/backfill; production hardening/rate limiting. Pet naming is intentionally not user-generated yet, avoiding moderation and HTML injection concerns.

Next projects: [boing.wall](https://github.com/Boing-Network/boing.wall), [boing.kudos](https://github.com/Boing-Network/boing.kudos), [boing.names](https://github.com/Boing-Network/boing.names). Their implementations follow Pets.

## Verification notes

Local lint, separate browser/Worker typechecks, build, 24 deterministic tests, local D1 migration and Worker dry-run are included. API tests use real SQLite transactions and a mocked testnet RPC; they never send chain transactions. The live RPC probe from the Windows machine failed with a TLS connection error during initial setup, so a real Express hatch/feed round trip still needs Nico's testnet-wallet approval and a reachable testnet RPC. Nothing was sent from a wallet by the implementation process.
Local lint, separate browser/Worker typechecks, build, deterministic tests, local D1 migration and Worker dry-run are included. API tests use SQLite and in-process Postgres (PGlite) with a mocked testnet RPC; they never send chain transactions. Read-only checks against `https://testnet-rpc.boing.network` returned chain 6913 from this environment. A real Express hatch/feed still needs Nico's testnet wallet. Nothing was sent from a wallet.

Worker binding types are generated from config: run `npm run types:generate` after config changes. Observability is configured for a future approved deployment; production hardening remains deferred.
72 changes: 72 additions & 0 deletions migrations/0002_yard.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
CREATE TABLE IF NOT EXISTS kennel (
id INTEGER PRIMARY KEY CHECK (id = 1),
contract TEXT NOT NULL UNIQUE,
deployer TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS yard_challenges (
id TEXT PRIMARY KEY,
owner TEXT NOT NULL,
action TEXT NOT NULL,
message TEXT NOT NULL,
amount TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
min_height INTEGER NOT NULL,
detail TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS pet_nfts (
owner TEXT PRIMARY KEY,
token_id TEXT NOT NULL,
contract TEXT NOT NULL,
mint_tx TEXT NOT NULL UNIQUE,
staked INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS stalls (
owner TEXT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('pen', 'stand', 'toy')),
created_at INTEGER NOT NULL,
PRIMARY KEY (owner, kind)
);
CREATE TABLE IF NOT EXISTS yards (
owner TEXT PRIMARY KEY,
kibble INTEGER NOT NULL,
settled_at INTEGER NOT NULL,
bonded INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS yard_events (
id TEXT PRIMARY KEY,
owner TEXT NOT NULL,
action TEXT NOT NULL,
tx_hash TEXT,
created_at INTEGER NOT NULL,
day INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS skills (
owner TEXT NOT NULL,
skill TEXT NOT NULL CHECK (skill IN ('forage', 'greet', 'play')),
points INTEGER NOT NULL,
last_day INTEGER NOT NULL,
PRIMARY KEY (owner, skill)
);
CREATE TABLE IF NOT EXISTS wardrobe (
id TEXT PRIMARY KEY,
owner TEXT NOT NULL,
item TEXT NOT NULL,
source TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS equipped (
owner TEXT NOT NULL,
slot TEXT NOT NULL CHECK (slot IN ('head', 'neck', 'body', 'back', 'feet')),
piece TEXT NOT NULL,
PRIMARY KEY (owner, slot)
);
CREATE TABLE IF NOT EXISTS presence (
owner TEXT PRIMARY KEY,
seen_at INTEGER NOT NULL,
x REAL,
z REAL,
pass TEXT,
pass_until INTEGER
);
Loading
Loading