Transparency
Public data model & audit trail
Every sale event, authentication event, and dispute on WATCHER is permanently recorded and publicly auditable. Below is the full database schema.
Every sale and authentication event is a link in a per-card hash chain — event_hash = sha256(seq, prevHash, event), anchored to the card's token hash. The app never edits or deletes events; corrections are new events. Any edit, deletion, or reorder breaks a hash and is detectable by anyone.
Each card publishes a signed record at /api/c/[token_hash]/provenance.json — the full event chain plus an Ed25519 signature over the chain head, with the public key at /.well-known/watcher-provenance.json. A dependency-light verifier recomputes the chain and checks the signature without trusting WATCHER's servers. On-chain anchoring of the head to Base is the remaining roadmap item.
There are no private sale events, hidden auth events, or internal dispute notes not visible on the provenance page. What you see on /c/[token_hash] is the complete record.
Nothing is on-chain yet. The plan below describes what we intend to deploy; no contract is live and no token has been minted.
A collector's Trust Score is derived only from facts already on the ledger — never opinions or off-platform reputation. trust-v2 weights the qualityof history, not just volume: holdings must age 30 days before counting fully, a sale claimed on-ledger by another account counts most, selling to distinct buyers beats repeat sales to one, tenure accrues slowly — and upheld disputes lower the score. Honest history moves it in both directions. Reporting a card stolen never lowers the reporter's score.
It's a banded heuristic for “how much on-ledger history does this account have,” not a guarantee about a person. Always read the full provenance before you buy.
The badge is computed from the same ledger facts, on every surface, by one shared rule: 30+ days on the ledger · 3+ holdings aged past 30 days OR 1+ sale claimed by the buyer's account · zero open or upheld disputes · complete public profile (@handle, avatar, bio). A dispute strips it automatically. It cannot be purchased.
Database Schema
Drizzle ORM · Neon Postgres## cards
| Column | Type | Notes |
|-----------------------|-------------------------------------------------------------------|------------------------------------|
| id | uuid, primary key | Auto-generated |
| token_hash | text, unique, indexed | Canonical URL identifier |
| on_chain_token_id | bigint, nullable | Reserved for future on-chain anchoring |
| contract_address | text, nullable | Null until minted |
| chain_id | integer, default 8453 | Base mainnet |
| binding_tier | enum('photo', 'sticker', 'chip') | |
| status | enum('authenticated', 'stale', 'disputed', 'burned') | |
| card_set | text | |
| card_number | text | |
| card_year | integer | |
| card_edition | text, nullable | |
| card_name | text | |
| grader | enum('PSA', 'BGS', 'CGC', 'SGC', 'ungraded'), nullable | |
| grader_cert_number | text, nullable | |
| grade | text, nullable | e.g. "10 GEM MINT" |
| hero_image_url | text | Public photo URL |
| fingerprint_hash | text | pHash of card surface image |
| fingerprint_vector_url| text, nullable | Pointer to vector in R2 |
| enrolled_at | timestamp | |
| last_verified_at | timestamp | |
| current_owner_id | uuid, foreign key → owners.id | |
| origin_story | text, nullable | Owner-attested narrative |
| head_hash | text, nullable | Provenance chain head (latest event_hash); null until first event |
| created_at | timestamp | |
| updated_at | timestamp | |
---
## owners
| Column | Type | Notes |
|---------------------|-----------------------------------------|-------------------------------------------|
| id | uuid, primary key | |
| clerk_user_id | text, unique | Links to Clerk auth record |
| wallet_address | text, nullable | Base wallet, set after onboarding |
| display_name | text | Default "Anonymous Owner #N" |
| is_named | boolean, default false | Opt-in to show real name |
| named_display_name | text, nullable | Real name if is_named = true |
| kyc_tier | enum('none', 'tier1', 'tier2') | |
| member_since | date | |
| created_at | timestamp | |
| updated_at | timestamp | |
---
## sale_events
| Column | Type | Notes |
|-------------------|-------------------------------------------------------------|-----------------------------------------|
| id | uuid, primary key | |
| card_id | uuid, foreign key → cards.id | |
| occurred_at | timestamp | |
| price_usd | decimal(12,2), nullable | Null if price_redacted = true |
| price_redacted | boolean, default false | |
| verification_tier | enum('on_chain', 'third_party', 'owner_attested') | |
| evidence_url | text, nullable | Auction link, eBay archive, tx hash |
| from_owner_id | uuid, foreign key → owners.id, nullable | |
| to_owner_id | uuid, foreign key → owners.id, nullable | |
| notes | text, nullable | |
| seq | integer, nullable | Position in the card's provenance chain |
| prev_hash | text, nullable | Previous event_hash (genesis = token_hash) |
| event_hash | text, nullable | sha256 over { seq, prev_hash, event } |
| created_at | timestamp | |
---
## auth_events
| Column | Type | Notes |
|---------------------|---------------------------------------------------------------------------------------------------|--------------------------------|
| id | uuid, primary key | |
| card_id | uuid, foreign key → cards.id | |
| event_type | enum('grade_verified', 'chip_enrolled', 'fingerprint_registered', 'nfc_tap', 'photo_reverify') | |
| occurred_at | timestamp | |
| evidence_url | text, nullable | |
| location | text, nullable | Only if user opts in |
| performer_owner_id | uuid, foreign key → owners.id, nullable | |
| metadata | jsonb | |
| seq | integer, nullable | Position in the card's provenance chain |
| prev_hash | text, nullable | Previous event_hash |
| event_hash | text, nullable | sha256 over { seq, prev_hash, event } |
| created_at | timestamp | |
---
## disputes
| Column | Type | Notes |
|---------------------|-----------------------------------------------------------------------------|------------------------|
| id | uuid, primary key | |
| card_id | uuid, foreign key → cards.id | |
| raised_by_owner_id | uuid, foreign key → owners.id | |
| reason | enum('wrong_source', 'wrong_card', 'wrong_timestamp', 'ambiguous_rule', 'other') | |
| evidence | text | |
| evidence_url | text, nullable | |
| status | enum('open', 'upheld', 'denied', 'escalated') | |
| resolution_notes | text, nullable | |
| raised_at | timestamp | |
| resolved_at | timestamp, nullable | |
| created_at | timestamp | |
---
## Provenance chain (tamper-evidence)
Each card's events form an append-only **hash chain**. Events are linked in the
order they were recorded:
```
genesis = sha256( canonical({ version, identity }) ) # binds card identity
event_hash[i] = sha256( canonical({ seq: i, prevHash, event }) )
prevHash[0] = genesis (identity anchor)
prevHash[i] = event_hash[i-1]
card.head_hash = event_hash[last]
```
The **genesis** is a hash of the card's immutable identity (token hash, name, set,
number, year, edition, grader, cert, grade, binding tier, fingerprint, enrolled-at),
so altering any of those breaks the entire chain. `canonical(...)` is a deterministic
serialization (object keys sorted, no whitespace, money as integer cents, dates as
ISO strings), so the same input always produces the same hash on any machine. Editing,
deleting, or reordering any event also changes a hash. Run `npm run verify:chains` to
confirm every card's chain recomputes, or verify a single card from its public
`provenance.json` (see `VERIFY.md`).
The chain head is Ed25519-signed (public key at `/.well-known/watcher-provenance.json`).
On-chain anchoring is still on the roadmap (see `/transparency`).