# Project export: ArtSoul Protocol

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Collector demand comes before minting: discover, bid, settle, then mint digital art on Base.
- Devpost: https://devpost.com/software/artsoul-protocol-tuylf7
- GitHub: https://github.com/maysonkiller/ArtSoul-Protocol
- Demo: https://artsoul.vercel.app/
- Video: https://www.youtube.com/embed/ZHviQqNBJNw?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — maysonkiller (116 commits), Claude Opus 4.8 (23 commits)

## Devpost submission (written by the team)

### Inspiration

Most NFT marketplaces mint artwork first and search for demand later. This creates speculative inventory, weak price discovery, and little context for collectors. ArtSoul reverses that order. Collector demand comes first, and minting happens only after a successful auction settlement.

### What it does

ArtSoul is a discovery-first digital art auction protocol built on Base. Artists publish artwork before minting and may receive optional AI value guidance for choosing a starting price. The guidance is advisory only and never controls auction settlement, ownership, floor price, fees, or royalties. Collectors discover artwork, signal interest, and participate in auctions. If an auction does not settle successfully, no NFT is created. After successful settlement, ArtSoul lazily mints the NFT to the First Collector and records transparent provenance: Creator First Collector Current Owner Auction and resale history Canonical floor established by settlement Public artwork, profiles, discovery pages, and protocol documentation can be explored without connecting a wallet.

### How we built it

ArtSoul combines: A responsive web frontend built with JavaScript, React, Vite, and a custom Classic/Future theme system Solidity smart contracts using Hardhat and OpenZeppelin Base Sepolia for the current public testnet Reown AppKit, WalletConnect, and injected wallet providers Supabase and PostgreSQL for application data A Node.js blockchain indexer for public projections and provenance Vercel serverless APIs and deployment Server-side AI value guidance that remains separate from protocol truth ArtSoul existed before OpenAI Build Week. During Build Week, Codex with GPT-5.6 was used as an engineering agent for evidence-led debugging, implementation, regression-test design, validation, documentation, and pull-request preparation. The founder supplied real iPhone reproduction steps and production wallet logs. Codex reconstructed the event timeline, isolated mobile WalletConnect session restoration and network-state races, implemented narrowly scoped fixes, added regression coverage, validated the production build, and prepared reviewable pull requests.

### Challenges we ran into

The largest Build Week challenge was mobile wallet reliability in external iPhone browsers. A mobile connection crosses several independent systems: the browser, Reown modal, WalletConnect relay, wallet application, iOS background suspension, session persistence, page restoration, and final chain validation. A wallet could approve the connection while the browser still had incomplete or stale state. We treated the wallet flow as a state machine instead of repeatedly patching the Connect button. Production logs were compared with AppKit, WalletConnect, and application state so each failure could be isolated without changing auction or contract semantics. Another challenge was ensuring that wallet identity restoration remained separate from operational network enforcement. ArtSoul can restore an existing session, but every write action must still require Base Sepolia. Accomplishments that we are proud of A working public testnet covering publishing, discovery, auctions, settlement, lazy minting, provenance, and resale Evidence-driven mobile wallet debugging using real-device production logs Regression tests for wallet session restoration and navigation races Base Sepolia write guards that protect every transaction path Public protocol documentation and an auditable indexer-backed data model A responsive interface supporting desktop and mobile layouts

### What we learned

Mobile wallet connection is not a single button action. It is a distributed session lifecycle that must survive background suspension, redirects, delayed settlement, page navigation, and network changes. We also learned that an engineering agent produces its strongest work when it receives concrete evidence, explicit invariants, narrow scope, and reproducible tests. ArtSoul keeps AI guidance separate from blockchain truth. AI may help a creator understand a possible starting value, but contracts and transaction receipts determine settlement, ownership, floor, and royalties.

### What's next

Complete the real-device mobile wallet verification matrix Finish security review and testnet validation Improve onboarding for the first artist and collector cohort Add public, indexer-backed protocol metrics Expand discovery-first community auction events Prepare for a future Base mainnet deployment after the testnet and audit phases are complete

## README (from the GitHub repository)

# ArtSoul

ArtSoul is an auction-first NFT art discovery protocol built on Base.

Artists publish artwork, the community discovers and signals interest, primary auctions establish the first collector and canonical floor, settlement lazily mints the NFT, and later resale preserves creator royalties and public provenance.

## OpenAI Build Week

ArtSoul is an existing protocol codebase that was extended during OpenAI Build Week. Work completed during the event is intentionally separated from earlier project history and can be reviewed through timestamped commits and pull requests.

### How Codex and GPT-5.6 were used

Codex with GPT-5.6 was used as the engineering agent for evidence-led debugging, implementation, regression-test design, validation, and pull-request preparation. The founder supplied real-device reproduction steps and on-screen production logs; Codex reconstructed event timelines, compared them with the wallet state machine, implemented narrowly scoped fixes, and verified the result against the repository canon.

The first confirmed Build Week change is the July 13, 2026 mobile-session reconciliation work in commit [`3d7fe3f`](https://github.com/maysonkiller/ArtSoul-Protocol/commit/3d7fe3fb40607e49585552e02f68230e9ec47ce9), merged through [PR #85](https://github.com/maysonkiller/ArtSoul-Protocol/pull/85). That work:

- reconciles delayed persisted WalletConnect sessions before final UI state is applied;
- preserves an already confirmed mobile session across page navigation and restore races;
- adds focused mobile wallet persistence and recovery regression coverage;
- separates bid error classification so wallet and auction errors produce accurate diagnostics;
- preserves the existing Base Sepolia write guard, auction lifecycle, settlement rules, and economics.

The implementation was validated with the production Vite build, Node syntax checks, focused Node test suites, and `git diff --check`. GPT-5.6 was used through Codex for the engineering workflow; it is not an auction pricing authority. ArtSoul's optional in-product AI value guidance remains guidance-only and cannot change settlement, floor, ownership, or royalties.

### Judge links and testing

- Live application: [artsoulprotocol.com](https://artsoulprotocol.com/)
- Public repository: [maysonkiller/ArtSoul-Protocol](https://github.com/maysonkiller/ArtSoul-Protocol)
- Protocol documentation: [Protocol Docs](https://artsoulprotocol.com/docs-protocol)

The public site can be explored without credentials. Wallet transactions currently target Base Sepolia testnet. A testnet wallet is required only for protected actions such as publishing or bidding; read-only artwork, discovery, protocol documentation, and profile surfaces remain publicly accessible.

## WebMCP Challenge

ArtSoul was entered in the WebMCP Challenge (25 August – 3 September 2026) as a
pre-existing project extended with WebMCP during the submission window. The
boundary is stated here so prior work is never presented as new.

**Existed before 25 August 2026:** the protocol canon and frozen economics, the
Solidity core and NFT contracts deployed on Base Sepolia, the fail-closed event
indexer, the public projection API, and the whole product interface — gallery,
artwork and auction pages, publishing, profiles and provenance.

**Added during the challenge window:** the agent interface. ArtSoul now declares
eleven tools with JSON Schema inputs to a WebMCP-capable browser, so an agent
reads auction state, provenance and lifecycle directly instead of inferring them
from the page. Any action that moves value ends at the person's own wallet: the
agent can open it with a prepared bid, and only after the person has granted that
permission in the page, but the wallet always asks them to approve, and no
website can delegate that click. New files: [`webmcp-tools.js`](webmcp-tools.js),
[`test/webmcp-tools.test.cjs`](test/webmcp-tools.test.cjs) and
[`docs/WEBMCP.md`](docs/WEBMCP.md). Changes to existing files are limited to one
build-manifest line and one deferred script tag on three pages. Every commit in
this work is prefixed `webmcp:` and dated inside the submission window.

No contract, economic rule, API route or protocol behavior was changed for the
challenge. How to enable WebMCP and what each tool answers is documented in
[`docs/WEBMCP.md`](docs/WEBMCP.md).

## Protocol Lifecycle

1. Creator uploads media and metadata.
2. Creator registers artwork on-chain.
3. Creator creates a primary auction.
4. Collectors bid during the auction window.
5. If the auction ends with no bids, no NFT is minted.
6. If the auction ends with a winner, settlement opens.
7. Successful settlement lazily mints the NFT to the First Collector.
8. The successful settlement creates the canonical floor.
9. Minted NFTs can later be listed for resale.
10. Provenance remains visible as Creator, First Collector, and Owner.

## Canon

The protocol canon lives in `docs/canon/`.

Current and completed work is tracked in the [durable project backlog](docs/BACKLOG.md). New ideas must be recorded there before implementation so chat threads do not become a parallel source of truth.

Important rules:

- Contracts and transaction receipts are protocol truth.
- `/api/public/artworks` is the public indexed source.
- Local pending state is only a temporary bridge while the indexer catches up.
- Legacy Supabase rows are compatibility/history only.
- ArtSoul is token-free: no token, no points, and no airdrop logic.
- Hidden anti-sybil implementation details are intentionally not stored in this public repository.

## Tech Stack

- Frontend: static HTML, React inline pages, CSS theme system.
- Wallet: AppKit / injected wallet providers.
- Contracts: Solidity, Hardhat, OpenZeppelin.
- Chain: Base Sepolia is the only active product testnet; Base is the canonical production chain. Historical Ethereum Sepolia records are read-only migration compatibility.
- Backend/API: Vercel serverless routes.
- Storage and database: Supabase/Postgres.
- Indexer: Node.js projection worker.

## Local Setup

Install dependencies:

```bash
npm install
```

Copy the environment template:

```bash
cp .env.example .env
```

Fill local values in `.env`. Never commit real secrets.

Run checks:

```bash
node --check src/api/routes/public/artworks.js
node --check src/indexer/production-runner.js
```

Run the indexer locally when needed:

```bash
set -a
source .env
set +a
node src/indexer/production-runner.js
```

On Windows PowerShell, set variables through `.env` tooling or the process manager used for your environment.

## License

ArtSoul uses a split licence.

- **Software** — contracts, indexer, API, frontend, scripts, tests, and operational documentation are licensed under the [Apache License, Version 2.0](LICENSE). You may use, modify, and redistribute them, including commercially, under the terms of that licence.
- **Protocol canon and brand** — `ARTSOUL_CANON_BIBLE_FULL.md`, `docs/canon/`, and the ArtSoul logo and brand assets are licensed separately under [LICENSE-DOCS](LICENSE-DOCS). They may be read and quoted with attribution, but not republished, adapted, or reused commercially.
- **Trademarks** — "ArtSoul" and the ArtSoul logo are not licensed by the Apache License (Section 6).

See [NOTICE](NOTICE) for the attribution summary required on redistribution.


## Detected evidence (automated analysis)

Indexed codebase: 301 recognized source files, 3048 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- PostgreSQL (technology) — detected in the code
- React (technology) — detected in the code
- Solidity (language) — detected in the code
- SQL (language) — detected in the code
- Tailwind CSS (technology) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- Supabase (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 391)

```
.env.example
.github/ISSUE_TEMPLATE/controlled-beta-bug.yml
.github/workflows/ci.yml
.gitignore
admin.html
AGENTS.md
ai-ui-styles.css
api/[...route].js
appkit-init.js
ARTSOUL_CANON_BIBLE_FULL.md
artwork-card-fixes.css
artwork.html
avatar-dropdown.js
button-effects.css
CLAUDE.md
contracts-config.js
contracts-integration.js
contracts/ArtSoulCore.sol
contracts/ArtSoulNFT.sol
contracts/ArtSoulProjectNFT.sol
contracts/interfaces/IArtSoulNFT.sol
contracts/interfaces/IArtSoulProjectNFT.sol
contracts/test/ReentrantWithdrawer.sol
design-system.css
docs-protocol.css
docs-protocol.html
docs/BACKLOG.md
docs/BUILD_STEP_PLAN.md
docs/canon/00_ARTSOUL_V4_1_CANON_BIBLE.md
docs/canon/05_PROVENANCE_UI_CANON.md
docs/canon/07_ADMIN_MODERATION_CANON.md
docs/canon/08_TOKEN_POLICY.md
docs/canon/12_IMPLEMENTATION_BACKLOG.md
docs/canon/13_CODEX_MASTER_PROMPT.md
docs/canon/14_PROTOCOL_REVENUE_AND_TEAM.md
docs/canon/16_VISUAL_STATUS_AND_THEME_CANON.md
docs/canon/17_ROADMAP_PHASES.md
docs/canon/ARTSOUL_CANON_BIBLE_FULL.md
docs/canon/CHANGELOG_2026-07-13_AMENDMENT.md
docs/canon/CHANGELOG_2026-07-16_ROADMAP_ALIGNMENT.md
docs/canon/CONTRACT_REWORK_PLAN.md
docs/canon/LICENSE
docs/canon/SUPPORT_THE_ARTIST_DESIGN.md
docs/HANDOFF.md
docs/progress/BASE_X_UPDATE_2026-07-17_TO_2026-07-21.md
docs/progress/PUBLIC_PROGRESS_2026-07-22_TO_2026-07-28.md
docs/progress/README.md
docs/PROJECT_STATE.md
docs/RESOURCE_GATED_WORK.md
docs/RPC_USAGE.md
docs/runbooks/A11_PUBLIC_METRICS_ROLLOUT.md
docs/runbooks/A8_MODERATION_ROLLOUT.md
docs/runbooks/A8A_PASSKEY_FOUNDATION.md
docs/runbooks/A8B_ARTWORK_REPORT_INTAKE.md
docs/runbooks/A8C_PROTOCOL_ADMIN_REVIEW.md
docs/runbooks/A8D_SAFE_RECOVERY.md
docs/runbooks/A9_INFRA_COST_MONITORING.md
docs/runbooks/PERMANENT_DOMAIN_CUTOVER.md
docs/security/A1_SECURITY_AND_MIGRATION_AUDIT.md
docs/security/MIGRATION_RUNBOOK.md
docs/SOCIAL_LINKING_SETUP.md
docs/testnet/A1_CREDENTIAL_HISTORY_ACCEPTANCE_2026-08-07.md
docs/testnet/A1_MOBILE_AUTH_UPLOAD_POLICY_ACCEPTANCE_2026-08-04.md
docs/testnet/A12_NETWORK_COPY_ACCEPTANCE.md
docs/testnet/BUG_REPORT_TEMPLATE.md
docs/testnet/CONTROLLED_BETA_ENTRY.md
docs/testnet/MOBILE_WALLET_PRODUCTION_ACCEPTANCE_2026-07-30.md
docs/testnet/PUBLIC_TESTNET_RUNBOOK.md
docs/testnet/RG01_APEX_ORIGIN_SMOKE_CHECKLIST.md
docs/testnet/RG05_SAFE_MULTISIG_REHEARSAL_2026-08-16.md
docs/testnet/SHARED_HEADER_PRODUCTION_ACCEPTANCE_2026-08-04.md
docs/testnet/SHARED_HEADER_PRODUCTION_REACCEPTANCE_2026-08-13.md
docs/testnet/TESTER_GUIDE.md
docs/testnet/WALLET_QA_CHECKLIST.md
docs/THEME_SYSTEM.md
docs/WALLET_MOBILE_ROOTCAUSE_TEST.md
gallery.html
generate-favicon.html
hardhat.config.cjs
header-prepaint.js
index.html
ipfs-client.js
LICENSE
LICENSE-DOCS
migrations/001_ai_integration.sql
mobile-no-motion.css
mobile-responsive.css
modal-system.js
NOTICE
oauth-integration.js
package-simple.json
package.json
performance-optimizations.css
profile.html
README.md
run-migration.sh
scripts/apply-migrations.js
scripts/apply-outbox-migration.js
scripts/apply-reorg-migration.js
scripts/chaos-test-1-db-crash.js
scripts/chaos-test-1-db-failure.js
scripts/chaos-test-2-worker-crash.js
scripts/chaos-test-3-race-conditions.js
scripts/chaos-test-4-backpressure.js
scripts/chaos-test-4-queue-overflow.js
scripts/chaos-test-5-real-overload.js
scripts/check-balance.js
scripts/check-indexer-health.mjs
scripts/convert-sql-placeholders.js
scripts/crash-simulation-test.js
scripts/deploy.js
scripts/graceful-shutdown-test.js
scripts/memory-leak-test.js
scripts/proof-zero-data-loss.js
scripts/run-migration-009.js
scripts/run-stability-tests.js
scripts/run-unit-tests.mjs
scripts/stress-test-advanced.js
scripts/stress-test-indexer.js
scripts/stress-test-simple.js
[271 more files omitted for size]
```

### Dependencies

- package.json: @nomicfoundation/hardhat-toolbox@^5.0.0, @openzeppelin/contracts@^5.0.0, @simplewebauthn/browser@^13.3.0, @simplewebauthn/server@^13.3.2, @vitejs/plugin-react@6.0.3, abbrev@^1.0.9, acorn@^8.16.0, acorn-walk@^8.3.5, adm-zip@^0.4.16, aes-js@^4.0.0-beta.5, agent-base@^6.0.2, aggregate-error@^3.1.0, ajv@^8.20.0, amdefine@^1.0.1, ansi-align@^3.0.1, ansi-colors@^4.1.3, ansi-escapes@^4.3.2, ansi-regex@^5.0.1, ansi-styles@^4.3.0, antlr4ts@^0.5.0-alpha.4, anymatch@^3.1.3, arg@^4.1.3, argparse@^2.0.1, array-back@^3.1.0, array-union@^2.1.0, array-uniq@^1.0.3, asap@^2.0.6, assertion-error@^1.1.0, astral-regex@^2.0.0, async@^1.5.2, asynckit@^0.4.0, at-least-node@^1.0.0, available-typed-arrays@^1.0.7, axios@^1.15.2, balanced-match@^1.0.2, base-x@^3.0.11, bech32@^1.1.4, binary-extensions@^2.3.0, blakejs@^1.2.1, bn.js@^5.2.3, boxen@^5.1.2, brace-expansion@^2.1.0, braces@^3.0.3, brorand@^1.1.0, browser-stdout@^1.3.1, browserify-aes@^1.2.0, bs58@^4.0.1, bs58check@^2.1.2, buffer-from@^1.1.2, buffer-xor@^1.0.3, bullmq@^5.76.8, bytes@^3.1.2, call-bind@^1.0.9, call-bind-apply-helpers@^1.0.2, call-bound@^1.0.4, camelcase@^6.3.0, caseless@^0.12.0, cbor@^8.1.0, chai@^4.5.0, chai-as-promised@^7.1.2, chalk@^4.1.2, charenc@^0.0.2, check-error@^1.0.3, chokidar@^4.0.3, ci-info@^2.0.0, cipher-base@^1.0.7, clean-stack@^2.2.0, cli-boxes@^2.2.1, cli-table3@^0.5.1, cliui@^7.0.4, color-convert@^2.0.1, color-name@^1.1.4, colors@^1.4.0, combined-stream@^1.0.8, command-exists@^1.2.9, command-line-args@^5.2.1, command-line-usage@^6.1.3, commander@^8.3.0, concat-map@^0.0.1, concat-stream@^1.6.2, cookie@^0.4.2, core-util-is@^1.0.3, cors@^2.8.6, create-hash@^1.2.0, create-hmac@^1.1.7, create-require@^1.1.1, crypt@^0.0.2, death@^1.1.0, debug@^4.4.3, decamelize@^4.0.0, deep-eql@^4.1.4, deep-extend@^0.6.0, deep-is@^0.1.4, define-data-property@^1.1.4, delayed-stream@^1.0.0, depd@^2.0.0, diff@^5.2.2, difflib@^0.2.4, dir-glob@^3.0.1, dotenv@^17.4.2, dunder-proto@^1.0.1, elliptic@^6.6.1, emoji-regex@^8.0.0, enquirer@^2.4.1, env-paths@^2.2.1, es-define-property@^1.0.1, es-errors@^1.3.0, es-object-atoms@^1.1.1, es-set-tostringtag@^2.1.0, escalade@^3.2.0, escape-string-regexp@^4.0.0, escodegen@^1.8.1, esprima@^2.7.3, estraverse@^1.9.3, esutils@^2.0.3, eth-gas-reporter@^0.2.27, ethereum-bloom-filters@^1.2.0, ethereum-cryptography@^0.1.3, ethereumjs-util@^7.1.5, ethers@^6.16.0, ethjs-unit@^0.1.6, evp_bytestokey@^1.0.3, express@^5.2.1, express-rate-limit@^8.5.2, fast-deep-equal@^3.1.3, fast-glob@^3.3.3, fast-levenshtein@^2.0.6, fast-uri@^3.1.0, fastq@^1.20.1, fill-range@^7.1.1, find-replace@^3.0.0, find-up@^5.0.0, flat@^5.0.2, follow-redirects@^1.16.0, for-each@^0.3.5, form-data@^4.0.5, fp-ts@^1.19.3, fs-extra@^10.1.0, fs-readdir-recursive@^1.1.0, fs.realpath@^1.0.0, function-bind@^1.1.2, get-caller-file@^2.0.5, get-func-name@^2.0.2, get-intrinsic@^1.3.0, get-port@^3.2.0, get-proto@^1.0.1, ghost-testrpc@^0.0.2, glob@^8.1.0, glob-parent@^5.1.2, global-modules@^2.0.0, global-prefix@^3.0.0, globby@^10.0.2, gopd@^1.2.0, graceful-fs@^4.2.11, handlebars@^4.7.9, hardhat@^2.28.6, hardhat-gas-reporter@^1.0.10, has-flag@^4.0.0, has-property-descriptors@^1.0.2, has-symbols@^1.1.0, has-tostringtag@^1.0.2, hash-base@^3.1.2, hash.js@^1.1.7, hasown@^2.0.3, he@^1.2.0, heap@^0.2.7, hmac-drbg@^1.0.1, http-basic@^8.1.3, http-errors@^2.0.1, http-response-object@^3.0.2, https-proxy-agent@^5.0.1, iconv-lite@^0.4.24, ignore@^5.3.2, immer@^10.0.2, immutable@^4.3.8, indent-string@^4.0.0, inflight@^1.0.6, inherits@^2.0.4, ini@^1.3.8, interpret@^1.4.0, io-ts@^1.10.4, ioredis@^5.10.1, is-binary-path@^2.1.0, is-callable@^1.2.7, is-extglob@^2.1.1, is-fullwidth-code-point@^3.0.0, is-glob@^4.0.3, is-hex-prefixed@^1.0.0, is-number@^7.0.0, is-plain-obj@^2.1.0, is-typed-array@^1.1.15, is-unicode-supported@^0.1.0, isarray@^2.0.5, isexe@^2.0.0, js-sha3@^0.8.0, js-yaml@^4.1.1, json-schema-traverse@^1.0.0, json-stream-stringify@^3.1.6, json-stringify-safe@^5.0.1, json5@^2.2.3, jsonfile@^6.2.1, jsonschema@^1.5.0, keccak@^3.0.4, kind-of@^6.0.3, kleur@^3.0.3, levn@^0.3.0, locate-path@^6.0.0, lodash@^4.17.21, lodash.camelcase@^4.3.0, lodash.clonedeep@^4.5.0, lodash.isequal@^4.5.0, lodash.truncate@^4.4.2, log-symbols@^4.1.0, loupe@^2.3.7, lru_map@^0.3.3, make-error@^1.3.6, markdown-table@^1.1.3, math-intrinsics@^1.1.0, md5.js@^1.3.5, memorystream@^0.3.1, merge2@^1.4.1, micro-eth-signer@^0.14.0, micro-ftch@^0.3.1, micro-packed@^0.7.3, micromatch@^4.0.8, mime-db@^1.52.0, mime-types@^2.1.35, minimalistic-assert@^1.0.1, minimalistic-crypto-utils@^1.0.1, minimatch@^5.1.9, minimist@^1.2.8, mkdirp@^0.5.6, mnemonist@^0.38.5, ms@^2.1.3, ndjson@^2.0.0, neo-async@^2.6.2, node-addon-api@^2.0.2, node-emoji@^1.11.0, node-gyp-build@^4.8.4, nofilter@^3.1.0, nopt@^3.0.6, normalize-path@^3.0.0, number-to-bn@^1.7.0, object-assign@^4.1.1, object-inspect@^1.13.4, obliterator@^2.0.5, once@^1.4.0, optionator@^0.8.3, ordinal@^1.0.3, os-tmpdir@^1.0.2, p-limit@^3.1.0, p-locate@^5.0.0, p-map@^4.0.0, parse-cache-control@^1.0.1, path-exists@^4.0.0, path-is-absolute@^1.0.1, path-parse@^1.0.7, path-type@^4.0.0, pathval@^1.1.1, pbkdf2@^3.1.5, pg@^8.20.0, picocolors@^1.1.1, picomatch@^2.3.2, pify@^4.0.1, possible-typed-array-names@^1.1.0, prelude-ls@^1.1.2, prettier@^2.8.8, process-nextick-args@^2.0.1, prom-client@^15.1.3, promise@^8.3.0, prompts@^2.4.2, proxy-from-env@^2.1.0, qs@^6.15.1, queue-microtask@^1.2.3, randombytes@^2.1.0, raw-body@^2.5.3, react@18.3.1, react-dom@18.3.1, readable-stream@^3.6.2, readdirp@^4.1.2, rechoir@^0.6.2, recursive-readdir@^2.2.3, reduce-flatten@^2.0.0, req-cwd@^2.0.0, req-from@^2.0.0, require-directory@^2.1.1, require-from-string@^2.0.2, resolve@^1.17.0, resolve-from@^3.0.0, reusify@^1.1.0, ripemd160@^2.0.3, rlp@^2.2.7, run-parallel@^1.2.0, safe-buffer@^5.2.1, safer-buffer@^2.1.2, sc-istanbul@^0.4.6, scrypt-js@^3.0.1, secp256k1@^4.0.4, semver@^6.3.1, serialize-javascript@^6.0.2, set-function-length@^1.2.2, setimmediate@^1.0.5, setprototypeof@^1.2.0, sha.js@^2.4.12, sha1@^1.1.1, shelljs@^0.8.5, side-channel@^1.1.0, side-channel-list@^1.0.1, side-channel-map@^1.0.1, side-channel-weakmap@^1.0.2, sisteransi@^1.0.5, slash@^3.0.0, slice-ansi@^4.0.0, solc@^0.8.26, solidity-coverage@^0.8.17, source-map@^0.2.0, source-map-support@^0.5.21, split2@^3.2.2, sprintf-js@^1.0.3, stacktrace-parser@^0.1.11, statuses@^2.0.2, string_decoder@^1.3.0, string-format@^2.0.0, string-width@^4.2.3, strip-ansi@^6.0.1, strip-hex-prefix@^1.0.0, strip-json-comments@^3.1.1, supports-color@^7.2.0, sync-request@^6.1.0, sync-rpc@^1.3.6, table@^6.9.0, table-layout@^1.0.2, tailwindcss@^3.4.19, then-request@^6.0.2, through2@^4.0.2, tinyglobby@^0.2.16, tmp@^0.0.33, to-buffer@^1.2.2, to-regex-range@^5.0.1, toidentifier@^1.0.1, ts-command-line-args@^2.5.1, ts-essentials@^7.0.3, ts-node@^10.9.2, tslib@^2.7.0, tsort@^0.0.1, type-check@^0.3.2, type-detect@^4.1.0, type-fest@^0.21.3, typechain@^8.3.2, typed-array-buffer@^1.0.3, typedarray@^0.0.6, typescript@^6.0.3, typical@^4.0.0, uglify-js@^3.19.3, undici@^5.29.0, undici-types@^7.19.2, universalify@^2.0.1, unpipe@^1.0.0, utf8@^3.0.0, util-deprecate@^1.0.2, uuid@^8.3.2, v8-compile-cache-lib@^3.0.1, vite@8.1.2, web3-utils@^1.10.4, which@^1.3.1, which-typed-array@^1.1.20, widest-line@^3.1.0, word-wrap@^1.2.5, wordwrap@^1.0.0, wordwrapjs@^4.0.1, workerpool@^6.5.1, wrap-ansi@^7.0.0, wrappy@^1.0.2, ws@^8.17.1, y18n@^5.0.8, yargs@^16.2.0, yargs-parser@^20.2.9, yargs-unparser@^2.0.0, yn@^3.1.1, yocto-queue@^0.1.0

### Recent commits (newest first)

- Merge pull request #197 from maysonkiller/docs/state-reconciliation-and-safe-rehearsal
- Reconcile project state and record the Safe rehearsal
- Merge pull request #196 from maysonkiller/codex/a8-safe-recovery
- Add Safe-only passkey recovery foundation
- Merge pull request #195 from maysonkiller/codex/a05-a46-production-reacceptance
- Record shared header production re-acceptance
- Merge pull request #194 from maysonkiller/codex/fix-mobile-cold-header
- Prevent cold header and artwork blank states
- Record shared header production acceptance (#193)
- Keep artwork moderation probes quiet (#192)
- Record the exact artwork cold path (#191)
- Keep the Tailwind build guard portable (#190)
- Stop the artwork page polling for a signal it already receives (#189)
- Record why lazy-loading the wallet SDK is a boot-contract change (#188)
- Correct A-47: the API was never the bottleneck (#187)
- Stop blocking first paint on the head script chain (#186)
- Document Phase C artist donations (#165)
- Record the measured load-performance findings as backlog items (#185)
- Compile Tailwind at build time instead of in the browser (#184)
- Guide a blocked publish instead of swallowing the click (#183)

## Key source files (fetched from GitHub, selected and truncated for size)

### ARTSOUL_CANON_BIBLE_FULL.md

```markdown
# ArtSoul Canon Bible

The canonical public protocol specification lives in:

- `docs/canon/ARTSOUL_CANON_BIBLE_FULL.md`
- split companion files in `docs/canon/00_*`, `05_*`, `07_*`, `08_*`, `12_*`, `13_*`, `14_*`, `16_*`, and `17_*`

This root file exists only as a pointer so agents and contributors can find the canon quickly.

Important public constraints:

- ArtSoul is a token-free NFT art discovery and auction protocol.
- Protocol truth comes from contracts, receipts, and V4.1 indexer projections.
- Public UI must not expose internal version labels.
- Hidden anti-sybil implementation details, scoring rules, and review tooling are intentionally excluded from the public repository.

```

### SECURITY_PUBLIC_READINESS_REPORT.md

```markdown
# Security And Public Repository Readiness Report

Date: 2026-06-26
Branch: `chore/repo-audit-cleanup`

## Summary

Task 1 cleanup reduces current-tree risk, removes legacy/token/business-unsafe material from the tracked tree, translates the public canon to English, and removes current hardcoded Supabase anon JWTs from browser source files.

Public release status: NO-GO until the human rotates affected keys and purges or replaces leaked history.

## Secret Scan

Tool used:

```text
gitleaks detect --source . --redact -v
```

Result: 13 redacted findings in git history.

Tracked current-tree scan: no secret-looking values found after cleanup.

Note: local ignored `.env` still exists on the workstation and contains real local secrets. It was not read into the report, modified, deleted, staged, or committed.

Historical findings:

| Commit | File | Finding |
| --- | --- | --- |
| `3cbfe488007f` | `.env.example:8` | `PRIVATE_KEY`-style value flagged by gitleaks |
| `3cbfe488007f` | `.env.example:3` | Supabase anon/public JWT |
| `3cbfe488007f` | `.env.example:4` | Supabase service-role JWT |
| `bda927123c82` | `PROJECT_SNAPSHOT_V4/core/oauth-integration.js:7` | Supabase public JWT |
| `bda927123c82` | `PROJECT_SNAPSHOT_V4/core/supabase-auth.js:5` | Supabase public JWT |
| `bda927123c82` | `PROJECT_SNAPSHOT_V4/core/supabase-client.js:6` | Supabase public JWT |
| `d6ef1845284f` | `SECURITY_AUDIT_REPORT.md:85` | Hardcoded Supabase key |
| `94f9168ef221` | `oauth-integration.js:7` | Supabase public JWT |
| `926bcc89424d` | `supabase-auth.js:5` | Supabase public JWT |
| `926bcc89424d` | `supabase-client.js:6` | Supabase public JWT |
| `0721229c3e11` | `supabase-client.js:6` | Supabase public JWT |
| `0721229c3e11` | `supabase-auth.js:5` | Supabase public JWT |
| `58b7ac0627c9` | `supabase-client.js:5` | Supabase publishable/public key |

No full secret values are included in this report.

## Keys The Human Must Rotate Or Verify Retired

- Supabase service-role key for project `bexigvqrunomwtjsxlej`.
- Supabase anon/publishable key for project `bexigvqrunomwtjsxlej`, or confirm the exposed historical key is no longer valid after RLS is closed.
- Any deployer/private key ever committed through `.env.example` or local deployment docs. If the historical value was only a placeholder, document that decision before public release.

Do not make the repository public until those rotations are complete.

## History Cleanup Required

The current branch removes current-tree exposures, but leaked values remain in git history. Before public release, the human should either:

- rewrite/purge history with an approved tool, then force-push intentionally; or
- create a fresh public repository with a clean history after secrets are rotated.

This task intentionally did not rewrite history.

## Current-Tree Cleanup

Removed from the tracked tree:

- Legacy and archived trees: `legacy/`, `archive/`.
- Token-only code and schema: `src/core/token-service.js`, `src/ui/token-bala
[truncated — 2985 more characters]
```

### package.json

```
{
  "name": "artsoul",
  "version": "1.0.0",
  "packageManager": "npm@11.6.2",
  "description": "Auction-first NFT art discovery protocol on Base.",
  "main": "appkit-init.js",
  "scripts": {
    "dev": "vite",
    "build": "npm run build:css && vite build && npm run verify:build && npm run verify:tailwind",
    "preview": "vite preview --host 0.0.0.0",
    "verify:build": "node scripts/verify-build.mjs",
    "monitor:indexer": "node scripts/check-indexer-health.mjs",
    "test": "npm run test:unit && npm run test:contracts",
    "test:unit": "node scripts/run-unit-tests.mjs",
    "test:contracts": "hardhat test test/ArtSoulV41.test.cjs",
    "build:css": "tailwindcss -c tailwind.config.cjs -i src/styles/tailwind.css -o tailwind-build.css --minify",
    "verify:tailwind": "node scripts/verify-tailwind.mjs"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/maysonkiller/ArtSoul-Protocol.git"
  },
  "keywords": [],
  "author": "",
  "license": "Apache-2.0",
  "type": "module",
  "bugs": {
    "url": "https://github.com/maysonkiller/ArtSoul-Protocol/issues"
  },
  "homepage": "https://github.com/maysonkiller/ArtSoul-Protocol#readme",
  "devDependencies": {
    "@nomicfoundation/hardhat-toolbox": "^5.0.0",
    "@openzeppelin/contracts": "^5.0.0",
    "@vitejs/plugin-react": "6.0.3",
    "hardhat": "^2.28.6",
    "tailwindcss": "^3.4.19",
    "vite": "8.1.2"
  },
  "directories": {
    "doc": "docs",
    "test": "test"
  },
  "dependencies": {
    "@simplewebauthn/browser": "^13.3.0",
    "@simplewebauthn/server": "^13.3.2",
    "abbrev": "^1.0.9",
    "acorn": "^8.16.0",
    "acorn-walk": "^8.3.5",
    "adm-zip": "^0.4.16",
    "aes-js": "^4.0.0-beta.5",
    "agent-base": "^6.0.2",
    "aggregate-error": "^3.1.0",
    "ajv": "^8.20.0",
    "amdefine": "^1.0.1",
    "ansi-align": "^3.0.1",
    "ansi-colors": "^4.1.3",
    "ansi-escapes": "^4.3.2",
    "ansi-regex": "^5.0.1",
    "ansi-styles": "^4.3.0",
    "antlr4ts": "^0.5.0-alpha.4",
    "anymatch": "^3.1.3",
    "arg": "^4.1.3",
    "argparse": "^2.0.1",
    "array-back": "^3.1.0",
    "array-union": "^2.1.0",
    "array-uniq": "^1.0.3",
    "asap": "^2.0.6",
    "assertion-error": "^1.1.0",
    "astral-regex": "^2.0.0",
    "async": "^1.5.2",
    "asynckit": "^0.4.0",
    "at-least-node": "^1.0.0",
    "available-typed-arrays": "^1.0.7",
    "axios": "^1.15.2",
    "balanced-match": "^1.0.2",
    "base-x": "^3.0.11",
    "bech32": "^1.1.4",
    "binary-extensions": "^2.3.0",
    "blakejs": "^1.2.1",
    "bn.js": "^5.2.3",
    "boxen": "^5.1.2",
    "brace-expansion": "^2.1.0",
    "braces": "^3.0.3",
    "brorand": "^1.1.0",
    "browser-stdout": "^1.3.1",
    "browserify-aes": "^1.2.0",
    "bs58": "^4.0.1",
    "bs58check": "^2.1.2",
    "buffer-from": "^1.1.2",
    "buffer-xor": "^1.0.3",
    "bullmq": "^5.76.8",
    "bytes": "^3.1.2",
    "call-bind": "^1.0.9",
    "call-bind-apply-helpers": "^1.0.2",
    "call-bound": "^1.0.4",
    "camelcase": "^6.3.0",
    "caseless": "^0.12.0",
    "cbor": "^8.1.0",
    "chai": "^4.5.0",
    "chai-as-promised": "^7.1.2",
    "chalk": "^4.1.2",
    "charenc": "^0.0.2",
    "check-error": "^1.0.3",
    "chokidar": "^4.0.3",
    "ci-info": "^2.0.0",
    "cipher-base": "^1.0.7",
    "clean-stack": "^2.2.0",
    "cli-boxes": "^2.2.1",
    "cli-table3": "^0.5.1",
    "cliui": "^7.0.4",
    "color-convert": "^2.0.1",
    "color-name": "^1.1.4",
    "colors": "^1.4.0",
    "combined-stream": "^1.0.8",
    "command-exists": "^1.2.9",
    "command-line-args": "^5.2.1",
    "command-line-usage": "^6.1.3",
    "commander": "^8.3.0",
    "concat-map": "^0.0.1",
    "concat-stream": "^1.6.2",
    "cookie": "^0.4.2",
    "core-util-is": "^1.0.3",
    "cors": "^2.8.6",
    "create-hash": "^1.2.0",
    "create-hmac": "^1.1.7",
    "create-require": "^1.1.1",
    "crypt": "^0.0.2",
    "death": "^1.1.0",
    "debug": "^4.4.3",
    "decamelize": "^4.0.0",
    "deep-eql": "^4.1.4",
    "deep-extend": "^0.6.0",
    "deep-is": "^0.1.4",
    "define-data-property": "^1.1.4",
    "delayed-stream": "^1.0.0",
    "depd": "^2.0.0",
    "diff": "^5.2.2",
    "difflib": "^0.2.4",
    "dir-glob": "^3.0.1",
    "dotenv": "^17.4.2",
    "dunder-proto": "^1.0.1",
    "elliptic": "^6.6.1",
    "emoji-regex": "^8.0.0",
    "enquirer": "^2.4.1",
    "env-paths": "^2.2.1",
    "es-define-property": "^1.0.1",
    "es-errors": "^1.3.0",
    "es-object-atoms": "^1.1.1",
    "es-set-tostringtag": "^2.1.0",
    "escalade": "^3.2.0",
    "escape-string-regexp": "^4.0.0",
    "escodegen": "^1.8.1",
    "esprima": "^2.7.3",
    "estraverse": "^1.9.3",
    "esutils": "^2.0.3",
    "eth-gas-reporter": "^0.2.27",
    "ethereum-bloom-filters": "^1.2.0",
    "ethereum-cryptography": "^0.1.3",
    "ethereumjs-util": "^7.1.5",
    "ethers": "^6.16.0",
    "ethjs-unit": "^0.1.6",
    "evp_bytestokey": "^1.0.3",
    "express": "^5.2.1",
    "express-rate-limit": "^8.5.2",
    "fast-deep-equal": "^3.1.3",
    "fast-glob": "^3.3.3",
    "fast-levenshtein": "^2.0.6",
    "fast-uri": "^3.1.0",
    "fastq": "^1.20.1",
    "fill-range": "^7.1.1",
    "find-replace": "^3.0.0",
    "find-up": "^5.0.0",
    "flat": "^5.0.2",
    "follow-redirects": "^1.16.0",
    "for-each": "^0.3.5",
    "form-data": "^4.0.5",
    "fp-ts": "^1.19.3",
    "fs-extra": "^10.1.0",
    "fs-readdir-recursive": "^1.1.0",
    "fs.realpath": "^1.0.0",
    "function-bind": "^1.1.2",
    "get-caller-file": "^2.0.5",
    "get-func-name": "^2.0.2",
    "get-intrinsic": "^1.3.0",
    "get-port": "^3.2.0",
    "get-proto": "^1.0.1",
    "ghost-testrpc": "^0.0.2",
    "glob": "^8.1.0",
    "glob-parent": "^5.1.2",
    "global-modules": "^2.0.0",
    "global-prefix": "^3.0.0",
    "globby": "^10.0.2",
    "gopd": "^1.2.0",
    "graceful-fs": "^4.2.11",
    "handlebars": "^4.7.9",
    "hardhat-gas-reporter": "^1.0.10",
    "has-flag": "^4.0.0",
    "has-property-descriptors": "^1.0.2",
    "has-symbols": "^1.1.0",
    "has-tostringtag": "^1.0.2",
    "hash-base": "^
[truncated — 6163 more characters]
```

### src/index.js

```javascript
// Services Index - Initialize and export all services
// This file sets up the service layer for the entire application

import AuctionService from './features/auction/auction-service.js';
import FileService from './features/artwork/file-service.js';
import ArtworkService from './features/artwork/artwork-service.js';
import { initializeUIComponents } from './ui/components/index.js';
import { NFTStatus, getStatusLabel, getStatusColor, isValidStatus } from './core/constants/nft-status.js';
import ErrorHandler from './core/utils/error-handler.js';
import LoadingHandler from './core/utils/loading-handler.js';
import {
    debounce,
    throttle,
    preventDoubleClick,
    enableLazyLoading,
    optimizeReactRender,
    preloadCriticalImages,
    setupIntersectionObserver,
    optimizeAnimations,
    initPerformanceOptimizations
} from './core/utils/performance-utils.js';
import './ai/index.js';
import './ui/ai-evaluation-panel.js';

/**
 * Initialize all services
 * Call this after contracts and supabase are initialized
 */
function initializeServices() {
    if (window.__artsoulServicesInitialized) return true;

    console.log('Initializing services...');

    // Check dependencies
    if (!window.ArtSoulContracts) {
        console.error('ArtSoulContracts not found. Load contracts-integration.js first.');
        return false;
    }

    if (!window.ArtSoulDB) {
        console.error('ArtSoulDB not found. Load supabase-client.js first.');
        return false;
    }

    try {
        // Make NFTStatus available globally
        window.NFTStatus = NFTStatus;
        window.getStatusLabel = getStatusLabel;
        window.getStatusColor = getStatusColor;
        window.isValidStatus = isValidStatus;
        console.log('NFTStatus constants initialized');

        // Initialize FileService (no dependencies)
        window.FileService = new FileService();
        console.log('FileService initialized');

        // Initialize AuctionService (depends on contracts)
        window.AuctionService = new AuctionService(window.ArtSoulContracts);
        console.log('AuctionService initialized');

        // Initialize ArtworkService (depends on supabase and auction service)
        window.ArtworkService = new ArtworkService(
            window.ArtSoulDB,
            window.AuctionService
        );
        console.log('ArtworkService initialized');

        // Initialize UI components
        initializeUIComponents();

        window.__artsoulServicesInitialized = true;
        console.log('All services initialized successfully');
        return true;
    } catch (error) {
        console.error('Failed to initialize services:', error);
        return false;
    }
}

async function initializeServicesWhenReady(maxWaitMs = 10000, pollMs = 50) {
    const deadline = Date.now() + maxWaitMs;
    while (Date.now() < deadline) {
        if (window.ArtSoulContracts && window.ArtSoulDB) {
            return initializeServices();
        }
        await new Promise(resolve => setTimeout(resolve, pollMs));
    }

    console.error('ArtSoul runtime dependencies did not initialize in time.');
    return false;
}

// Auto-initialize when DOM is ready
if (typeof window !== 'undefined') {
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            void initializeServicesWhenReady();
        });
    } else {
        void initializeServicesWhenReady();
    }
}

// Export for manual initialization if needed
export {
    initializeServices,
    initializeServicesWhenReady,
    AuctionService,
    FileService,
    ArtworkService,
    NFTStatus,
    ErrorHandler,
    LoadingHandler
};

```

### src/ai/index.js

```javascript
/**
 * AI Services Index
 *
 * Exports all AI modules:
 * - NFTValuationEngine: Price estimation for NFTs
 * - AuctionInsightEngine: Bid pattern analysis
 * - SystemDebugAssistant: Log and error analysis
 */

// Import AI modules
import './nft-valuation-engine.js';
import './auction-insight-engine.js';
import './system-debug-assistant.js';

// Initialize AI services
class AIServices {
    constructor() {
        this.valuation = new window.NFTValuationEngine();
        this.auction = new window.AuctionInsightEngine();
        this.debug = new window.SystemDebugAssistant();

        console.log('🤖 AI Services initialized');
    }

    /**
     * Evaluate NFT price
     */
    async evaluateNFT(nft) {
        return await this.valuation.evaluate(nft);
    }

    /**
     * Analyze auction bids
     */
    async analyzeAuction(auction) {
        return await this.auction.analyze(auction);
    }

    /**
     * Diagnose system issue
     */
    diagnoseIssue(input) {
        return this.debug.diagnose(input);
    }

    /**
     * Generate system health report
     */
    generateHealthReport(logs) {
        return this.debug.generateReport(logs);
    }

    /**
     * Detect bottlenecks
     */
    detectBottlenecks(logs) {
        return this.debug.detectBottlenecks(logs);
    }
}

// Export singleton
window.AIServices = new AIServices();

console.log('🤖 AI Services module loaded');

```

### src/indexer/index.js

```javascript
import EventListener from './event-listener.js';
import IndexerSyncEngine from './sync-engine.js';
import cacheService from '../services/cache-service.js';

class ArtSoulIndexer {
    constructor(config) {
        this.config = config;
        this.db = config.database;
        this.chainId = Number(config.chainId || 0);

        this.eventListener = new EventListener({
            rpcUrl: config.rpcUrl,
            readRpcUrls: config.readRpcUrls,
            contractAddress: config.contractAddress,
            chainId: this.chainId
        });

        this.syncEngine = new IndexerSyncEngine(this.db, this.eventListener);

        console.log('[ArtSoulIndexer] Initialized');
        console.log('  Contract:', config.contractAddress);
        console.log('  Chain:', this.chainId);
        console.log('  Start Block:', config.startBlock);
    }

    _chainIdString() {
        return this.chainId.toString();
    }

    _scopedCacheKey(type, id) {
        return `${this._chainIdString()}:${type}:${id}`;
    }

    _timeValue(value) {
        if (!value) {
            return null;
        }
        return value instanceof Date ? value.getTime() : Number(value);
    }

    async start() {
        console.log('[ArtSoulIndexer] Starting indexer...');

        await this.syncEngine.initialize(
            this.config.contractAddress,
            this.chainId,
            this.config.startBlock
        );

        const state = await this.syncEngine.getIndexerState();
        const currentBlock = await this.eventListener.getCurrentBlock();

        console.log(`[ArtSoulIndexer] Current state:`);
        console.log(`  Last indexed block: ${state.last_indexed_block}`);
        console.log(`  Current block: ${currentBlock}`);
        console.log(`  Blocks behind: ${currentBlock - state.last_indexed_block}`);

        if (currentBlock > state.last_indexed_block) {
            console.log('[ArtSoulIndexer] Syncing historical events...');
            const eventCount = await this.syncEngine.syncHistoricalEvents(
                state.last_indexed_block + 1,
                currentBlock
            );
            console.log(`[ArtSoulIndexer] Synced ${eventCount} historical events`);
        }

        await this.syncEngine.start();

        console.log('[ArtSoulIndexer] Indexer started and listening for new events');
    }

    async stop() {
        console.log('[ArtSoulIndexer] Stopping indexer...');
        await this.syncEngine.stop();
        console.log('[ArtSoulIndexer] Indexer stopped');
    }

    async getAuction(artworkId) {
        const cacheKey = cacheService.keys.auction(this._scopedCacheKey('artwork', artworkId));
        return cacheService.remember(cacheKey, 3600, async () => {
            const auction = await this.db.query(
                `SELECT a.*, art.metadata_uri, art.minted, art.canonical_floor
                 FROM v41_auctions a
                 LEFT JOIN v41_artworks art ON art.chain_id = a.chain_id AND art.artwork_id = a.artwork_id
                 WHERE a.chain_id = $1 AND a.artwork_id = $2
                 ORDER BY a.auction_id DESC
                 LIMIT 1`,
                [this._chainIdString(), artworkId]
            );

            if (auction.length === 0) {
                return null;
            }

            return {
                artworkId: auction[0].artwork_id,
                auctionId: auction[0].auction_id,
                creator: auction[0].creator,
                startingPrice: auction[0].start_price,
                duration: auction[0].duration,
                endTime: this._timeValue(auction[0].end_time),
                settlementDeadline: this._timeValue(auction[0].settlement_deadline),
                status: auction[0].status,
                settled: auction[0].status === 'settled',
                defaulted: auction[0].status === 'defaulted' || auction[0].status === 'defaulted_no_bids',
                currentBidder: auction[0].current_bidder,
                currentBid: auction[0].current_bid,
                winner: auction[0].winner,
                winningBid: auction[0].winning_bid,
                tokenId: auction[0].token_id,
                canonicalFloor: auction[0].canonical_floor,
                minted: auction[0].minted,
                blockNumber: auction[0].block_number,
                lastUpdatedBlock: auction[0].last_updated_block
            };
        });
    }

    async getBids(artworkId) {
        const cacheKey = cacheService.keys.bids(this._scopedCacheKey('artwork', artworkId));
        return cacheService.remember(cacheKey, 3600, async () => {
            const bids = await this.db.query(
                `SELECT * FROM v41_bids
                 WHERE chain_id = $1 AND artwork_id = $2
                 ORDER BY block_number ASC, log_index ASC`,
                [this._chainIdString(), artworkId]
            );

            return bids.map(bid => ({
                auctionId: bid.auction_id,
                bidder: bid.bidder,
                amount: bid.bid_amount,
                depositAmount: bid.deposit_amount,
                timestamp: this._timeValue(bid.indexed_at),
                blockNumber: bid.block_number,
                transactionHash: bid.transaction_hash
            }));
        });
    }

    async getActiveAuctions() {
        const now = Date.now();

        const auctions = await this.db.query(
            `SELECT * FROM v41_auctions
             WHERE chain_id = $1
             AND status = 'active'
             AND end_time > to_timestamp($2 / 1000.0)
             ORDER BY end_time ASC`,
            [this._chainIdString(), now]
        );

        return auctions.map(a => ({
            auctionId: a.auction_id,
            artworkId: a.artwork_id,
            creator: a.creator,
            startingPrice: a.start_price,
            endTime: this._timeValue(a.end_time),
            currentBidder: a.current_bidder,
            currentBid: a.current_bid
        }));
    }

    async getEnded
[truncated — 6311 more characters]
```

### src/ui/components/index.js

```javascript
// UI Components Index
// Export all UI components

import BackButton from './BackButton.js';
import ProfileButton from './ProfileButton.js';

/**
 * Initialize UI components
 */
function initializeUIComponents() {
    if (window.__artsoulUIComponentsInitialized) return true;
    window.__artsoulUIComponentsInitialized = true;

    console.log(' Initializing UI components...');

    // Make components globally available
    window.BackButton = BackButton;
    window.ProfileButton = ProfileButton;

    console.log('UI components initialized');
    return true;
}

// Auto-initialize when DOM is ready
if (typeof window !== 'undefined') {
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeUIComponents);
    } else {
        initializeUIComponents();
    }
}

export { BackButton, ProfileButton, initializeUIComponents };

```

### src/api/server.js

```javascript
import express from 'express';
import PostgreSQLDatabase from '../indexer/postgresql-database.js';
import ModerationService from '../features/moderation/moderation-service.js';
import RBACService from '../features/moderation/rbac-service.js';
import CryptographicAuditLog from '../features/moderation/cryptographic-audit-log.js';
import createModerationAPI from '../features/moderation/moderation-api.js';
import createIndexerAPI from '../indexer/indexer-api.js';
import ArtSoulIndexer from '../indexer/index.js';
import cors from 'cors';
import dotenv from 'dotenv';
import { verifyMessage } from 'ethers';
import crypto from 'crypto';
import session from 'express-session';
import rateLimit from 'express-rate-limit';
import { resolveIndexerChainConfigs } from '../indexer/chain-config.js';
import { validateSiweMessage } from './backend.js';

dotenv.config();

function readEnv(names) {
    for (const name of names) {
        const value = process.env[name];
        if (value && value.trim()) return value.trim();
    }
    return '';
}

function requireEnv(names, label) {
    const value = readEnv(names);
    if (!value) {
        throw new Error(`${label} is required. Configure one of: ${names.join(', ')}`);
    }
    return value;
}

function resolveApiIndexerConfig() {
    const chains = resolveIndexerChainConfigs();
    if (chains.length > 1) {
        throw new Error(
            `Multiple API indexer chains configured (${chains.map(chain => chain.slug).join(', ')}). ` +
            'Run one API/indexer process per chain until API aggregation is enabled.'
        );
    }

    const chain = chains[0];

    return {
        rpcUrl: chain.rpcUrl,
        readRpcUrls: chain.readRpcUrls,
        contractAddress: chain.coreAddress,
        chainId: chain.chainId,
        startBlock: chain.startBlock
    };
}

const app = express();
const PORT = process.env.PORT || 3000;

function allowedApiOrigins() {
    const configured = readEnv(['API_ALLOWED_ORIGINS', 'OAUTH_ALLOWED_ORIGINS']);
    const origins = configured
        ? configured.split(',').map(origin => origin.trim()).filter(Boolean)
        : ['https://artsoulprotocol.com', 'https://artsoul.vercel.app'];

    if (process.env.NODE_ENV !== 'production') {
        origins.push('http://localhost:5173', 'http://127.0.0.1:5173');
    }

    return new Set(origins);
}

const apiOrigins = allowedApiOrigins();

// Middleware
app.use(cors({
    credentials: true,
    origin(origin, callback) {
        if (!origin || apiOrigins.has(origin)) {
            callback(null, true);
            return;
        }
        callback(new Error('CORS origin is not allowed'));
    }
}));
app.use(express.json());
app.use((req, res, next) => {
    res.setHeader('Cache-Control', 'private, no-store');
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('Referrer-Policy', 'same-origin');
    next();
});

const globalApiLimiter = rateLimit({
    windowMs: 60 * 1000,
    max: 60,
    message: { error: 'Too many requests', code: 'RATE_LIMIT_EXCEEDED' },
    standardHeaders: true,
    legacyHeaders: false,
});

app.use(session({
    secret: requireEnv(['SESSION_SECRET'], 'SESSION_SECRET'),
    resave: false,
    saveUninitialized: false,
    cookie: {
        secure: process.env.NODE_ENV === 'production',
        httpOnly: true,
        sameSite: 'lax'
    }
}));

const authLimiter = rateLimit({
    windowMs: 60 * 1000,
    max: 5,
    message: { error: 'Too many authentication attempts', code: 'AUTH_RATE_LIMIT_EXCEEDED' },
    standardHeaders: true,
    legacyHeaders: false,
});

// Nonces for SIWE are now handled in the DB (siwe_nonces table)

// Initialize database
const database = new PostgreSQLDatabase({
    connectionString: requireEnv(['DATABASE_URL'], 'DATABASE_URL')
});

// Initialize services
const rbacService = new RBACService(database);
const auditLog = new CryptographicAuditLog(database);
await auditLog.initialize();

const moderationService = new ModerationService(database, rbacService, auditLog);

// Initialize indexer (read-only mode for API)
const apiIndexerConfig = resolveApiIndexerConfig();
const indexer = new ArtSoulIndexer({
    database: database,
    rpcUrl: apiIndexerConfig.rpcUrl,
    readRpcUrls: apiIndexerConfig.readRpcUrls,
    contractAddress: apiIndexerConfig.contractAddress,
    chainId: apiIndexerConfig.chainId,
    startBlock: apiIndexerConfig.startBlock
});

// --- AUTH LAYER (EIP-4361 / SIWE) ---
app.get('/auth/nonce', authLimiter, async (req, res) => {
    const wallet = normalizeWalletAddress(req.query.wallet);
    if (!wallet) {
        return res.status(400).json({ error: 'INVALID_WALLET' });
    }

    try {
        const nonce = crypto.randomUUID();
        const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes expiry

        await database.query(
            `INSERT INTO siwe_nonces (nonce, wallet, expires_at) VALUES ($1, $2, $3)`,
            [nonce, wallet, expiresAt]
        );

        res.json({ nonce });
    } catch (error) {
        console.error('[Auth] Nonce generation failed:', error);
        res.status(500).json({ error: 'Internal server error' });
    }
});

app.post('/auth/verify', authLimiter, async (req, res) => {
    try {
        const { message, signature, address, nonce } = req.body;
        if (!message || !signature || !address || !nonce) {
            return res.status(400).json({ error: 'Missing message, signature, address, or nonce' });
        }

        const walletAddr = normalizeWalletAddress(address);
        if (!walletAddr) {
            return res.status(400).json({ error: 'INVALID_WALLET' });
        }

        // 1. Bind the signed SIWE message to this request before verifying it.
        validateSiweMessage(req, {
            message,
            wallet: walletAddr,
            nonce
        });

        // 2. Cryptographic verification
        const recovered = verifyMessage(message, signature);
        if (recovered.toLowerCase() !== walletAddr) {
            r
[truncated — 15808 more characters]
```

### src/core/engine/index.js

```javascript
import AuctionEngine from './auction-engine.js';
import OfferEngine from './offer-engine.js';
import StateResolver from './state-resolver.js';
import VisibilityEngine from './visibility-engine.js';
import RiskEngine from './risk-engine.js';

class CoreMarketplaceEngine {
    constructor() {
        this.auctionEngine = new AuctionEngine();
        this.offerEngine = new OfferEngine();
        this.stateResolver = new StateResolver();
        this.visibilityEngine = new VisibilityEngine();
        this.riskEngine = new RiskEngine();
    }

    getArtworkState(artworkData, auctionData, offerData, walletData, currentTime) {
        const state = this.stateResolver.resolveState(
            artworkData,
            auctionData,
            offerData,
            currentTime
        );

        const metadata = this.stateResolver.getStateMetadata(
            state,
            artworkData,
            auctionData,
            offerData,
            currentTime
        );

        const visibility = {
            inGallery: this.visibilityEngine.isVisibleInGallery(artworkData, state),
            isFeatured: this.visibilityEngine.isFeatured(artworkData),
            isCurated: this.visibilityEngine.isCurated(artworkData)
        };

        const risk = walletData ? {
            canBid: !this.riskEngine.isWalletRateLimited(
                walletData.address,
                'bid',
                walletData.actionHistory,
                currentTime
            ),
            canOffer: !this.riskEngine.isWalletRateLimited(
                walletData.address,
                'offer',
                walletData.actionHistory,
                currentTime
            ),
            riskScore: this.riskEngine.calculateWalletRiskScore(
                walletData.address,
                walletData.behaviorHistory
            )
        } : null;

        return {
            state,
            metadata,
            visibility,
            risk
        };
    }

    validateAction(action, artworkData, auctionData, walletData, actionData, currentTime) {
        const state = this.stateResolver.resolveState(
            artworkData,
            auctionData,
            null,
            currentTime
        );

        if (action === 'bid') {
            if (state !== 'AUCTION') {
                return { valid: false, reason: 'Auction is not active' };
            }

            if (!this.auctionEngine.isValidBidAmount(
                actionData.bidAmount,
                auctionData.highestBid,
                artworkData.floorPrice
            )) {
                return { valid: false, reason: 'Bid amount too low' };
            }

            if (walletData && this.riskEngine.isWalletRateLimited(
                walletData.address,
                'bid',
                walletData.actionHistory,
                currentTime
            )) {
                return { valid: false, reason: 'Rate limit exceeded' };
            }

            return { valid: true };
        }

        if (action === 'offer') {
            if (state !== 'FOR_SALE') {
                return { valid: false, reason: 'Artwork is not listed for resale' };
            }

            if (!this.offerEngine.isValidOffer(
                actionData.offerAmount,
                artworkData.floorPrice,
                actionData.currentHighestOffer
            )) {
                return { valid: false, reason: 'Offer amount too low' };
            }

            if (walletData && this.riskEngine.isWalletRateLimited(
                walletData.address,
                'offer',
                walletData.actionHistory,
                currentTime
            )) {
                return { valid: false, reason: 'Rate limit exceeded' };
            }

            return { valid: true };
        }

        if (action === 'purchase') {
            if (state === 'SETTLEMENT_PENDING') {
                const metadata = this.stateResolver.getStateMetadata(
                    state,
                    artworkData,
                    auctionData,
                    null,
                    currentTime
                );

                if (walletData && walletData.address !== metadata.buyer) {
                    return { valid: false, reason: 'Not the designated buyer' };
                }

                if (currentTime > metadata.settlementDeadline) {
                    return { valid: false, reason: 'Settlement window expired' };
                }

                return { valid: true };
            }

            if (state === 'FOR_SALE') {
                return { valid: true };
            }

            return { valid: false, reason: 'Artwork not available for purchase' };
        }

        return { valid: false, reason: 'Unknown action' };
    }
}

export default CoreMarketplaceEngine;

if (typeof window !== 'undefined') {
    window.CoreMarketplaceEngine = CoreMarketplaceEngine;
}

```

### styles.css

```css
@import url("design-system.css?v=1");
@import url("unified-styles.css?v=2");

body {
    min-height: 100vh;
}

```

[280 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]