# Project export: ThriftChain

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: Cal Hacks 12.0
- Tagline: Thrifting shouldn’t cost you 30%. ThriftChain is a decentralized secondhand marketplace where users trade directly, keep full profits, avoid platform fees, and discover items through AI-understanding.
- Devpost: https://devpost.com/software/thriftchain-vluacx
- GitHub: https://github.com/rijulg06/ThriftChain/
- Demo: https://thrift-chain.vercel.app/
- Team: 3 GitHub contributor(s) — rohinsood (24 commits), sahith-p (18 commits), Rijul Garg (17 commits)

## Devpost submission (written by the team)

### Inspiration

Our team has always enjoyed thrifting, whether it’s for personal style, sustainability, or the excitement of finding something truly original. But when we tried to bring that experience online, we encountered a different reality: listings buried by algorithms, paywalls for visibility, and platforms taking up to a third of every sale. What began as a shared hobby made us realize a larger issue—people who bring value to resale platforms do not control their earnings, audience, or data. ThriftChain was created to make online resale feel like in-person thrifting again: community-focused, accessible, and fair to the people who power it.

### What it does

On-Chain Ownership & Zero-Fee Trading: Listings exist as smart contracts on Sui, eliminating intermediaries and fees so sellers keep 100% of earnings. On-Chain Negotiation & Bidding: Buyers and sellers negotiate directly through on-chain offers and counteroffers with a permanent, transparent audit trail. Semantic AI Search: Vector-based search enables discovery by style and aesthetic meaning, bypassing keyword limitations and paid promotion. Decentralized Image Storage (Walrus): Images are stored on a fault-tolerant decentralized network with permanent, tamper-proof links stored on-chain. Smart Contract Escrow: Payments are held in on-chain escrow until buyer confirmation, enabling trustless transactions with automated protection for both parties.

### How we built it

Architecture ThriftChain is built on a four-layer decentralized architecture that eliminates traditional marketplace intermediaries. At the foundation, we deployed smart contracts written in Move language on the Sui blockchain to handle item listings, escrow transactions, and reputation management with fast finality and minimal gas costs. For storage, we integrated Walrus Protocol to host all item images as decentralized blobs, with their IDs stored directly in our on-chain objects, keeping the entire stack Web3-native without relying on centralized services. The frontend is a Next.js 16 application with React 19 and TypeScript, featuring dual authentication through both Suiet wallet extensions for crypto users and Enoki's zkLogin OAuth for seamless Google sign-in. To power intelligent discovery, we built a semantic search layer using Supabase's PostgreSQL with pgvector extension, storing 1536-dimensional OpenAI embeddings for item titles, descriptions, and images that enable users to search by meaning rather than keywords. All three layers—blockchain, decentralized storage, and AI search—communicate through the Mysten SDK ecosystem, creating a fully trustless marketplace where users can "find vintage jackets" instead of scrolling through generic keyword results. Project Workflow We started by designing and deploying the core Move smart contracts to Sui testnet, implementing marketplace primitives like item creation, bidding, and escrow mechanics that leverage Sui's object-oriented model for direct peer-to-peer transfers. Once the blockchain foundation was solid, we built the Next.js frontend with Tailwind CSS and shadcn/ui components, connecting it to Sui through the TypeScript SDK and implementing both wallet-based and OAuth authentication flows. In parallel, we set up the Supabase database with the pgvector extension and created an indexing pipeline that generates OpenAI embeddings whenever new items are listed on-chain, linking each vector to its corresponding Sui object ID for hybrid queries. The integration phase connected all components: when a user lists an item, images upload to Walrus and return blob IDs that get stored in the smart contract, which triggers our backend to generate embeddings and index them in Supabase for semantic search. We then implemented the search interface that queries the vector database for similar items and fetches complete blockchain data including Walrus image references, creating a seamless experience where AI discovery meets decentralized ownership. Throughout the 48-hour hackathon, we iterated rapidly on the UI to achieve a retro thrift aesthetic while ensuring the complex blockchain interactions remained intuitive for both crypto-native users and newcomers exploring Web3 for the first time.

### Challenges we ran into

Our biggest technical hurdle emerged from Next.js 16's new turbopack bundler fundamentally changing how BCS (Binary Canonical Serialization) values are encoded when calling Sui smart contracts—a six-hour debugging marathon from 10PM to 4AM that forced us to deeply understand how tx.pure serialization works rather than relying on automatic type inference. We ultimately resolved this by explicitly typing every transaction parameter using methods like tx.pure.string(), tx.pure.u64(), and tx.pure.vector('string', tags) instead of letting the bundler serialize values automatically, which turbopack handled differently than webpack due to internal string handling optimizations. Walrus integration presented its own challenges when testnet endpoints changed mid-development and broke our entire image storage pipeline, prompting us to build a resilient solution with environment-based configuration for both publisher and aggregator URLs, comprehensive retry logic with detailed logging at each stage, validation functions to catch oversized files before upload, and health check endpoints to gracefully handle downtime. Perhaps the most frustrating limitation was the lack of mature end-to-end testing tools for Move smart contracts—we couldn't reliably test transaction flows locally, so we built our own TypeScript-based testing framework that signs transactions with exported keypairs, parses deployment IDs from config files, validates on-chain state changes, and provides colorized console output to debug complex multi-step flows like offer creation and escrow settlement. These workarounds, while time-consuming, taught us the bleeding edge of blockchain development isn't always polished, and sometimes the best path forward is building your own infrastructure when the ecosystem's tooling doesn't exist yet.

### Accomplishments we're proud of

A lot of us have heard of blockchain and understood it as a scary piece of technical jargon, but literally 2 days ago we actual learned how to use it. . It was also our first hackathon as a team, and we spent long nights learning, experimenting, and building side by side. What started as a fun idea quickly became something meaningful, because it reflected our real interests and the kind of experiences we care about.

### What we learned

Building ThriftChain taught us invaluable lessons about the realities of blockchain development beyond traditional web applications. We discovered that decentralization requires creative compromises—pure blockchain solutions can be too slow for modern UX expectations, necessitating hybrid architectures that preserve trustlessness while delivering speed. The fragility of emerging blockchain ecosystems became apparent when testnet endpoints changed unexpectedly and serialization libraries conflicted across languages. Most significantly, we learned that blockchain development tooling still lags behind traditional frameworks, requiring manual workarounds for basic features like end-to-end testing. These challenges reinforced that building on cutting-edge technology demands resilience, adaptability, and deep understanding of low-level protocols.

### What's next

ThriftChain's roadmap focuses on expanding functionality while improving the developer and user experience. We plan to implement native mobile applications for iOS and Android, making sustainable shopping more accessible to mainstream consumers. Enhanced AI-powered recommendation systems will suggest items based on user preferences and sustainability metrics, while automated pricing suggestions will help sellers optimize their listings. We're exploring integration with additional blockchain networks beyond Sui to increase marketplace liquidity and reach. Finally, we aim to contribute back to the ecosystem by developing open-source testing frameworks for Move smart contracts and publishing our hybrid architecture patterns to help other builders navigate the decentralization-performance tradeoff.

## README (from the GitHub repository)

# ThriftChain

A decentralized marketplace built on Sui blockchain for peer-to-peer trading of thrift items with AI-powered personalization.

## Project Structure

```
ThriftChain/
├── frontend/          # Next.js frontend application
│   ├── src/          # Source code
│   ├── public/       # Static assets
│   └── package.json  # Frontend dependencies
├── contracts/        # Sui Move smart contracts (to be added)
├── tasks/            # Development task lists
├── docs/             # Documentation
└── package.json      # Root workspace configuration
```

## Getting Started

### Prerequisites

- Node.js 18+ 
- npm or yarn

### Installation

1. Clone the repository
2. Install frontend dependencies:
```bash
npm run install:frontend
# or
cd frontend && npm install
```

### Development

Run the development server from the root:

```bash
npm run dev
# or navigate to frontend directory
cd frontend && npm run dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

### Available Scripts

From the root directory:
- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run start` - Start production server
- `npm run lint` - Run ESLint

From the `frontend/` directory:
- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run start` - Start production server
- `npm run lint` - Run ESLint

## Features

### Wallet Integration
- **Suiet Wallet Kit**: Connect with Sui wallet extensions
- **zkLogin (Enoki)**: Passwordless authentication via Google OAuth
- Routes:
  - `GET /api/zklogin/enoki/start` - Initiate OAuth flow
  - `POST /api/zklogin/enoki/complete` - Complete authentication
  - `GET /auth/callback` - OAuth callback handler

### Environment Variables

Create a `.env.local` file in the `frontend/` directory:

```env
# Enoki (managed zkLogin)
ENOKI_API_KEY=your_enoki_api_key
ENOKI_OAUTH_CLIENT_ID=your_google_oauth_client_id
NEXT_PUBLIC_ZKLOGIN_REDIRECT_URL=http://localhost:3000/auth/callback

# Supabase (to be added)
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key

# Sui Network
NEXT_PUBLIC_SUI_NETWORK=testnet
NEXT_PUBLIC_RPC_URL=https://fullnode.testnet.sui.io:443
```

## Tech Stack

- **Frontend**: Next.js 16, React 19, TypeScript
- **Styling**: Tailwind CSS, shadcn/ui
- **Blockchain**: Sui Network, @mysten/sui SDK
- **Wallet**: Suiet Wallet Kit, Enoki zkLogin
- **Storage**: Walrus
- **Database**: Supabase

## Learn More

- [Next.js Documentation](https://nextjs.org/docs)
- [Sui Documentation](https://docs.sui.io/)
- [Supabase Documentation](https://supabase.com/docs)
- [ThriftChain PRD](./PRD_Final.md)

## Deployment

The frontend can be deployed to Vercel. See [Next.js deployment docs](https://nextjs.org/docs/app/building-your-application/deploying).

# Docs Repos
 - https://github.com/MystenLabs/sui
 - https://github.com/MystenLabs/move-book
 - https://github.com/MystenLabs/ts-sdks
 - https://github.com/MystenLabs/walrus-docs


## Detected evidence (automated analysis)

Indexed codebase: 114 recognized source files, 754 KB.
- CSS (language) — detected in the code
- Google Gemini (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 155)

```
.gitignore
check-data.mjs
check-similarity-scores.mjs
CLAUDE.md
contracts/marketplace/deploy.log
contracts/marketplace/e2e-tests/API_INTEGRATION.md
contracts/marketplace/e2e-tests/cases/cases.md
contracts/marketplace/e2e-tests/cases/PAYMENT_TESTS.md
contracts/marketplace/e2e-tests/cases/README.md
contracts/marketplace/e2e-tests/cases/TC-ESCRO-ALL.sh
contracts/marketplace/e2e-tests/cases/TC-ESCROW-001.sh
contracts/marketplace/e2e-tests/cases/TC-ESCROW-002.sh
contracts/marketplace/e2e-tests/cases/TC-ESCROW-003.sh
contracts/marketplace/e2e-tests/cases/TC-LIST-001.sh
contracts/marketplace/e2e-tests/cases/TC-LIST-002.sh
contracts/marketplace/e2e-tests/cases/TC-LIST-003.sh
contracts/marketplace/e2e-tests/cases/TC-LIST-004.sh
contracts/marketplace/e2e-tests/cases/TC-LIST-005.sh
contracts/marketplace/e2e-tests/cases/TC-LIST-ALL.sh
contracts/marketplace/e2e-tests/cases/TC-OFFER-001.sh
contracts/marketplace/e2e-tests/cases/TC-OFFER-002.sh
contracts/marketplace/e2e-tests/cases/TC-OFFER-003.sh
contracts/marketplace/e2e-tests/cases/TC-OFFER-004.sh
contracts/marketplace/e2e-tests/cases/TC-OFFER-005.sh
contracts/marketplace/e2e-tests/cases/TC-OFFER-ALL.sh
contracts/marketplace/e2e-tests/cases/TC-PAYMENT-001.sh
contracts/marketplace/e2e-tests/cases/TC-PAYMENT-002.sh
contracts/marketplace/e2e-tests/cases/TC-PAYMENT-003.sh
contracts/marketplace/e2e-tests/cases/TC-PAYMENT-004.sh
contracts/marketplace/e2e-tests/cases/TC-PAYMENT-005.sh
contracts/marketplace/e2e-tests/cases/TC-PAYMENT-ALL.sh
contracts/marketplace/e2e-tests/cases/TEST_CASE_DOCUMENTATION.md
contracts/marketplace/e2e-tests/devnet_ids.txt
contracts/marketplace/e2e-tests/devnet_ids.txt.backup.20251026_011520
contracts/marketplace/e2e-tests/devnet_ids.txt.backup.20251026_014121
contracts/marketplace/e2e-tests/devnet_ids.txt.backup.20251026_044601
contracts/marketplace/e2e-tests/devnet_ids.txt.backup.20251026_053638
contracts/marketplace/e2e-tests/helper/create_item.sh
contracts/marketplace/e2e-tests/helper/README.md
contracts/marketplace/e2e-tests/init-test.txt
contracts/marketplace/e2e-tests/README.md
contracts/marketplace/e2e-tests/TC-LIST-README.md
contracts/marketplace/e2e-tests/testnet_ids.txt
contracts/marketplace/e2e-tests/TYPESCRIPT_GUIDE.md
contracts/marketplace/Move.lock
contracts/marketplace/Move.toml
contracts/marketplace/PAYMENT_QUICK_START.md
contracts/marketplace/scripts/deploy.sh
contracts/marketplace/scripts/export_frontend_env.sh
contracts/marketplace/scripts/README.md
contracts/marketplace/scripts/upgrade.sh
contracts/marketplace/sources/thriftchain.move
contracts/marketplace/tests/thriftchain_tests.move
contracts/README.md
create-test-item.mjs
debug-search.mjs
diagnose-search.mjs
docs/integration-plan.md
final-demo.mjs
frontend/check-embeddings.js
frontend/components.json
frontend/dev-server.log
frontend/env.test.js
frontend/eslint.config.mjs
frontend/next.config.js
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/public/mockdata.csv
frontend/SETUP.md
frontend/src/app/api/ai/cleanup-invalid/route.ts
frontend/src/app/api/ai/index-item/route.ts
frontend/src/app/api/ai/search/route.ts
frontend/src/app/api/hello/route.ts
frontend/src/app/api/items/link-sui-id/route.ts
frontend/src/app/api/test-gemini/route.ts
frontend/src/app/api/walrus/upload/route.ts
frontend/src/app/api/zklogin/complete/route.ts
frontend/src/app/api/zklogin/enoki/complete/route.ts
frontend/src/app/api/zklogin/enoki/start/route.ts
frontend/src/app/api/zklogin/start-flow/route.ts
frontend/src/app/auth/callback/CallbackClient.tsx
frontend/src/app/auth/callback/page.tsx
frontend/src/app/debug/page.tsx
frontend/src/app/globals.css
frontend/src/app/items/[id]/page.tsx
frontend/src/app/layout.tsx
frontend/src/app/list-item/page.tsx
frontend/src/app/listings/page.tsx
frontend/src/app/not-found.tsx
frontend/src/app/page.tsx
frontend/src/app/providers.tsx
frontend/src/app/stash/page.tsx
frontend/src/components/CompactItemCard.tsx
frontend/src/components/Header.tsx
frontend/src/components/ItemCard.tsx
frontend/src/components/ItemForm.tsx
frontend/src/components/LoginModal.tsx
frontend/src/components/MakeOfferModal.tsx
frontend/src/components/ui/alert-dialog.tsx
frontend/src/components/ui/button.tsx
frontend/src/components/ui/dialog.tsx
frontend/src/components/ui/sonner.tsx
frontend/src/lib/ai/embeddings.ts
frontend/src/lib/data/mock-listings.ts
frontend/src/lib/data/mock-offers.ts
frontend/src/lib/sui/client.ts
frontend/src/lib/sui/manual-transaction.ts
frontend/src/lib/sui/queries.ts
frontend/src/lib/sui/rpc-transactions.ts
frontend/src/lib/sui/transactions.ts
frontend/src/lib/supabase/client.ts
frontend/src/lib/supabase/database.types.ts
frontend/src/lib/supabase/items.ts
frontend/src/lib/supabase/server.ts
frontend/src/lib/types/sui-objects.ts
frontend/src/lib/utils.ts
frontend/src/lib/walrus/upload.ts
frontend/SUPABASE_SETUP.md
frontend/supabase-schema.sql
[35 more files omitted for size]
```

### Dependencies

- frontend/package.json: @google/generative-ai@^0.24.1, @mysten/dapp-kit@^0.19.6, @mysten/enoki@^0.3.0, @mysten/sui@1.36.0, @mysten/walrus@^0.8.1, @radix-ui/react-alert-dialog@^1.1.15, @radix-ui/react-dialog@^1.1.1, @radix-ui/react-slot@^1.2.3, @suiet/wallet-kit@^0.5.0, @supabase/realtime-js@^2.76.1, @supabase/ssr@^0.7.0, @supabase/supabase-js@^2.76.1, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, babel-plugin-react-compiler@1.0.0, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9, eslint-config-next@16.0.0, lucide-react@^0.548.0, next@16.0.0, next-themes@^0.4.6, pixelarticons@^1.8.1, react@19.2.0, react-dom@19.2.0, sonner@^2.0.7, tailwind-merge@^3.3.1, tailwindcss@^4, tw-animate-css@^1.4.0, typescript@^5
- package.json: @google/generative-ai@^0.24.1, @mysten/dapp-kit@^0.19.6, @mysten/enoki@^0.3.0, @mysten/sui@1.36.0, @mysten/walrus@^0.8.1, @radix-ui/react-alert-dialog@^1.1.15, @radix-ui/react-dialog@^1.1.1, @radix-ui/react-slot@^1.2.3, @suiet/wallet-kit@^0.5.0, @supabase/realtime-js@^2.76.1, @supabase/ssr@^0.7.0, @supabase/supabase-js@^2.76.1, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, babel-plugin-react-compiler@1.0.0, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9, eslint-config-next@16.0.0, lucide-react@^0.548.0, next@16.0.0, next-themes@^0.4.6, pixelarticons@^1.8.1, react@19.2.0, react-dom@19.2.0, sonner@^2.0.7, tailwind-merge@^3.3.1, tailwindcss@^4, tw-animate-css@^1.4.0, typescript@^5

### Recent commits (newest first)

- Clarify storage and database integration in README
- a
- wlalrus fix
- fixes
- fixes
- feat: implement counter offer functionality
- fix: improve offer creation error handling and logging
- remove vercel config
- vercel
- remove package lock
- vercel fix
- fix
- fix: remove turbopack config and add vercel.json for webpack builds
- fix: wrap useSearchParams in Suspense boundary for Vercel build
- deploy fixes
- fix package prepare for deploy
- fix: resolve item creation and blockchain fetching errors
- significant changes to blockchain functionality with frontend
- deployment w/updated smart contract
- Merge branch 'main' of https://github.com/rijulg06/ThriftChain

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

### process-task-list.md

```markdown
# Task List Management

Guidelines for managing task lists in markdown files to track progress on completing a PRD

## Task Implementation
- **One sub-task at a time:** Do **NOT** start the next sub‑task until you ask the user for permission and they say "yes" or "y"
- **Completion protocol:**  
  1. When you finish a **sub‑task**, immediately mark it as completed by changing `[ ]` to `[x]`.
  2. If **all** subtasks underneath a parent task are now `[x]`, follow this sequence:
    - **First**: Run the full test suite (`pytest`, `npm test`, `bin/rails test`, etc.)
    - **Only if all tests pass**: Stage changes (`git add .`)
    - **Clean up**: Remove any temporary files and temporary code before committing
    - **Commit**: Use a descriptive commit message that:
      - Uses conventional commit format (`feat:`, `fix:`, `refactor:`, etc.)
      - Summarizes what was accomplished in the parent task
      - Lists key changes and additions
      - References the task number and PRD context
      - **Formats the message as a single-line command using `-m` flags**, e.g.:

        ```
        git commit -m "feat: add payment validation logic" -m "- Validates card type and expiry" -m "- Adds unit tests for edge cases" -m "Related to T123 in PRD"
        ```
  3. Once all the subtasks are marked completed and changes have been committed, mark the **parent task** as completed.
- Stop after each sub‑task and wait for the user's go‑ahead.

## Task List Maintenance

1. **Update the task list as you work:**
   - Mark tasks and subtasks as completed (`[x]`) per the protocol above.
   - Add new tasks as they emerge.

2. **Maintain the "Relevant Files" section:**
   - List every file created or modified.
   - Give each file a one‑line description of its purpose.

## AI Instructions

When working with task lists, the AI must:

1. Regularly update the task list file after finishing any significant work.
2. Follow the completion protocol:
   - Mark each finished **sub‑task** `[x]`.
   - Mark the **parent task** `[x]` once **all** its subtasks are `[x]`.
3. Add newly discovered tasks.
4. Keep "Relevant Files" accurate and up to date.
5. Before starting work, check which sub‑task is next.
6. After implementing a sub‑task, update the file and then pause for user approval.
```

### PROJECT_STRUCTURE.md

```markdown
# ThriftChain Project Structure

## Overview

ThriftChain has been refactored into a clean workspace structure with the frontend separated into its own directory. This allows for future expansion with additional modules like backend services, smart contracts, and shared libraries.

## Directory Structure

```
ThriftChain/
├── frontend/                    # Next.js frontend application
│   ├── src/                    # Source code
│   │   ├── app/                # Next.js app directory
│   │   │   ├── api/            # API routes
│   │   │   ├── auth/           # Authentication pages
│   │   │   └── ...             # Other pages
│   │   ├── components/         # React components
│   │   │   ├── ui/             # shadcn/ui components
│   │   │   └── ...             # Custom components
│   │   └── lib/                # Utilities and helpers
│   ├── public/                 # Static assets
│   ├── .next/                  # Next.js build output (gitignored)
│   ├── node_modules/           # Frontend dependencies (gitignored)
│   ├── package.json            # Frontend dependencies
│   ├── tsconfig.json           # TypeScript config for frontend
│   ├── next.config.ts          # Next.js configuration
│   ├── postcss.config.mjs      # PostCSS configuration
│   └── components.json         # shadcn/ui configuration
│
├── contracts/                  # Sui Move smart contracts
│   ├── marketplace/
│   │   ├── sources/
│   │   │   └── thriftchain.move  # Main marketplace contract
│   │   └── Move.toml            # Move package configuration
│   └── README.md
│
├── tasks/                      # Development task lists
│   └── tasks-PRD_Final.md      # Task breakdown from PRD
│
├── docs/                       # Documentation
│   ├── move-book/              # Move language documentation
│   └── sui/                    # Sui blockchain documentation
│
├── node_modules/               # Root workspace dependencies
├── .gitignore                  # Git ignore rules
├── package.json                # Root workspace configuration
├── tsconfig.json               # Root TypeScript configuration
├── README.md                   # Main project README
├── PRD_Final.md                # Product Requirements Document
└── PROJECT_STRUCTURE.md        # This file
```

## Running the Application

### From Root Directory

```bash
# Install dependencies (will also install frontend deps)
npm install

# Run development server
npm run dev

# Build for production
npm run build

# Start production server
npm run start

# Run linter
npm run lint
```

### From Frontend Directory

```bash
cd frontend

# Install dependencies
npm install

# Run development server
npm run dev

# Build for production
npm run build

# Start production server
npm run start

# Run linter
npm run lint
```

## Key Files

### Root Level

- `package.json` - Workspace configuration with scripts to manage frontend
- `tsconfig.json` - Root TypeScript configuration
- `.gitignore` - Git ignore rules for both root and frontend
[truncated — 2101 more characters]
```

### package.json

```
{
  "name": "thriftchain",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --webpack",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@google/generative-ai": "^0.24.1",
    "@mysten/dapp-kit": "^0.19.6",
    "@mysten/enoki": "^0.3.0",
    "@mysten/sui": "1.36.0",
    "@mysten/walrus": "^0.8.1",
    "@radix-ui/react-alert-dialog": "^1.1.15",
    "@radix-ui/react-dialog": "^1.1.1",
    "@radix-ui/react-slot": "^1.2.3",
    "@suiet/wallet-kit": "^0.5.0",
    "@supabase/realtime-js": "^2.76.1",
    "@supabase/ssr": "^0.7.0",
    "@supabase/supabase-js": "^2.76.1",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^0.548.0",
    "next": "16.0.0",
    "next-themes": "^0.4.6",
    "pixelarticons": "^1.8.1",
    "react": "19.2.0",
    "react-dom": "19.2.0",
    "sonner": "^2.0.7",
    "tailwind-merge": "^3.3.1"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "babel-plugin-react-compiler": "1.0.0",
    "eslint": "^9",
    "eslint-config-next": "16.0.0",
    "tailwindcss": "^4",
    "tw-animate-css": "^1.4.0",
    "typescript": "^5"
  },
  "overrides": {
    "@mysten/sui": "1.36.0"
  }
}

```

### frontend/package.json

```
{
  "name": "thriftchain",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --webpack",
    "build": "next build --webpack",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@google/generative-ai": "^0.24.1",
    "@mysten/dapp-kit": "^0.19.6",
    "@mysten/enoki": "^0.3.0",
    "@mysten/sui": "1.36.0",
    "@mysten/walrus": "^0.8.1",
    "@radix-ui/react-alert-dialog": "^1.1.15",
    "@radix-ui/react-dialog": "^1.1.1",
    "@radix-ui/react-slot": "^1.2.3",
    "@suiet/wallet-kit": "^0.5.0",
    "@supabase/realtime-js": "^2.76.1",
    "@supabase/ssr": "^0.7.0",
    "@supabase/supabase-js": "^2.76.1",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^0.548.0",
    "next": "16.0.0",
    "next-themes": "^0.4.6",
    "pixelarticons": "^1.8.1",
    "react": "19.2.0",
    "react-dom": "19.2.0",
    "sonner": "^2.0.7",
    "tailwind-merge": "^3.3.1"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "babel-plugin-react-compiler": "1.0.0",
    "eslint": "^9",
    "eslint-config-next": "16.0.0",
    "tailwindcss": "^4",
    "tw-animate-css": "^1.4.0",
    "typescript": "^5"
  },
  "overrides": {
    "@mysten/sui": "1.36.0"
  }
}

```

### frontend/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono, Press_Start_2P, Space_Mono } from "next/font/google";
import "./globals.css";
import Providers from "./providers";
import { Header } from "@/components/Header";
import { Toaster } from "@/components/ui/sonner";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

const pressStart = Press_Start_2P({
  weight: "400",
  variable: "--font-retro",
  subsets: ["latin"],
});

const spaceMono = Space_Mono({
  weight: ["400", "700"],
  variable: "--font-retro-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable} ${pressStart.variable} ${spaceMono.variable} antialiased font-mono`}>
        <Providers>
          <Header />
          {children}
          <Toaster />
        </Providers>
      </body>
    </html>
  );
}

```

### frontend/src/app/page.tsx

```typescript
"use client"
import { useRouter } from "next/navigation"
import { useState, useEffect } from "react"
import { CompactItemCard } from "@/components/CompactItemCard"
import type { ItemCardProps } from "@/components/ItemCard"
import { getAllItems } from "@/lib/sui/queries"
import { ItemStatus } from "@/lib/types/sui-objects"

export default function Home() {
  const router = useRouter()
  const [q, setQ] = useState("")
  const [items, setItems] = useState<ItemCardProps[]>([])

  useEffect(() => {
    // Load items for the carousel
    const loadItems = async () => {
      try {
        const allItems = await getAllItems(undefined, { limit: 50 })
        const activeItems = allItems.data.filter(item => item.fields.status === ItemStatus.Active)

        const mapped: ItemCardProps[] = activeItems.map(item => ({
          objectId: item.objectId,
          title: item.fields.title,
          priceMist: BigInt(item.fields.price),
          category: item.fields.category,
          walrusImageIds: item.fields.walrus_image_ids || [],
          seller: item.fields.seller,
        }))

        const shuffled = [...mapped].sort(() => Math.random() - 0.5)
        setItems(shuffled.slice(0, 20))
      } catch (error) {
        console.error('Failed to load marketplace items:', error)
      }
    }
    loadItems()
  }, [])

  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault()
    const query = q.trim()
    if (query) {
      router.push(`/listings?q=${encodeURIComponent(query)}`)
    } else {
      router.push('/listings')
    }
  }

  return (
    <div className="max-h-screen">
      <section className="mx-auto max-w-5xl px-6 pt-24">
        <div className="retro-card retro-shadow p-5">
          <h1 className="text-3xl sm:text-5xl tracking-tight">
            Discover and trade on-chain thrift finds
          </h1>
          <p className="mt-3 opacity-80 max-w-2xl">
            ThriftChain is a decentralized marketplace on Sui. Own your listings, store images on Walrus, and search by meaning using AI-powered semantic search.
          </p>
        </div>

        <div className="mt-6 flex flex-col sm:flex-row gap-3">
          <form
            className="flex-1 flex items-center gap-2 retro-card retro-shadow px-3 py-2"
            onSubmit={handleSearch}
          >
            <input
              value={q}
              onChange={(e) => setQ(e.target.value)}
              placeholder="Search by meaning: 'vintage leather jacket'"
              className="flex-1 bg-transparent outline-none text-base"
            />
            <button type="submit" className="text-sm px-4 py-2 bg-black text-white dark:bg-white dark:text-black retro-btn">
              Search
            </button>
          </form>

          <a href="/listings" className="text-sm flex items-center justify-center px-4 py-2 border-2 border-black bg-white dark:bg-zinc-950 text-center retro-btn">
            Browse
          </a>
          <a href="/list-item" className="text-sm flex items-center justify-center px-4 py-2 border-2 border-black bg-white dark:bg-zinc-950 text-center retro-btn">
            List Item
          </a>
        </div>
      </section>

      {/* Infinite Scrolling Item Carousel */}
      {items.length > 0 && (
        <section className="overflow-hidden">
          <div className="mb-6 text-center mt-20">
          </div>
          
          {/* Scrolling Container */}
          <div className="relative">
            {/* First loop */}
            <div className="flex gap-6 animate-scroll-infinite">
              {items.map((item, index) => (
                <div key={`first-${index}`} className="flex-shrink-0">
                  <CompactItemCard {...item} />
                </div>
              ))}
              {/* Duplicate for seamless loop */}
              {items.map((item, index) => (
                <div key={`duplicate-${index}`} className="flex-shrink-0">
                  <CompactItemCard {...item} />
                </div>
              ))}
            </div>
          </div>
        </section>
      )}
    </div>
  )
}

```

### frontend/src/app/list-item/page.tsx

```typescript
"use client"

import { ItemForm } from "@/components/ItemForm"

export default function ListItemPage() {
  return (
    <div className="min-h-screen">
      <div className="mx-auto max-w-3xl px-6 pt-24 pb-16">
        {/* Header */}
        <div className="retro-card retro-shadow p-5 mb-6">
          <h1 className="text-3xl sm:text-4xl tracking-tight">
            List Your Item
          </h1>
          <p className="mt-2 opacity-80">
            Create a blockchain-backed listing with decentralized image storage
          </p>
        </div>

        {/* Form */}
        <ItemForm />
      </div>
    </div>
  )
}

```

### frontend/src/lib/supabase/server.ts

```typescript
import { createClient } from '@supabase/supabase-js'
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

// Get environment variables
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!

if (!supabaseUrl || !supabaseAnonKey) {
  throw new Error('Missing Supabase environment variables. Please check your .env.local file.')
}

/**
 * Server-side Supabase client for use in API routes and Server Components.
 * This client handles server-side authentication and cookie management.
 * 
 * Usage in API Routes:
 * ```ts
 * import { getSupabaseServerClient } from '@/lib/supabase/server'
 * 
 * export async function GET() {
 *   const supabase = getSupabaseServerClient()
 *   const { data, error } = await supabase.from('items').select('*')
 *   return Response.json({ data, error })
 * }
 * ```
 * 
 * Usage in Server Components:
 * ```ts
 * import { getSupabaseServerClient } from '@/lib/supabase/server'
 * 
 * export default async function Page() {
 *   const supabase = getSupabaseServerClient()
 *   const { data } = await supabase.from('items').select('*')
 *   return <div>{data}</div>
 * }
 * ```
 */
export function getSupabaseServerClient() {
  return createServerClient(supabaseUrl, supabaseAnonKey, {
    cookies: {
      async getAll() {
        const store = await cookies()
        return store.getAll()
      },
      async setAll(cookiesToSet) {
        try {
          const store = await cookies()
          cookiesToSet.forEach(({ name, value, options }) =>
            store.set(name, value, options)
          )
        } catch {
          // The `setAll` method was called from a Server Component.
          // This can be ignored if you have middleware refreshing
          // user sessions.
        }
      },
    },
  })
}

/**
 * Admin Supabase client using service role key for elevated permissions.
 * Use this ONLY in secure server-side contexts (API routes, server actions).
 * 
 * WARNING: This client bypasses Row Level Security (RLS) policies.
 * 
 * Usage:
 * ```ts
 * import { getSupabaseAdminClient } from '@/lib/supabase/server'
 * 
 * // In API route
 * const supabase = getSupabaseAdminClient()
 * const { data } = await supabase.from('users').select('*')
 * ```
 */
export function getSupabaseAdminClient() {
  const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!
  
  if (!serviceRoleKey) {
    throw new Error('Missing SUPABASE_SERVICE_ROLE_KEY environment variable.')
  }

  return createClient(supabaseUrl, serviceRoleKey, {
    auth: {
      autoRefreshToken: false,
      persistSession: false,
    },
  })
}


```

### frontend/src/app/debug/page.tsx

```typescript
'use client';

import { useState } from 'react';
import { suiClient } from '@/lib/sui/client';

export default function DebugPage() {
  const [output, setOutput] = useState<string>('');
  const [loading, setLoading] = useState(false);

  const checkMarketplace = async () => {
    setLoading(true);
    setOutput('');
    try {
      const MARKETPLACE_ID = process.env.NEXT_PUBLIC_MARKETPLACE_ID!;
      const PACKAGE_ID = process.env.NEXT_PUBLIC_THRIFTCHAIN_PACKAGE_ID!;

      let logs = [];
      logs.push(`📦 Package ID: ${PACKAGE_ID}`);
      logs.push(`🏪 Marketplace ID: ${MARKETPLACE_ID}`);
      logs.push('');

      // Fetch marketplace object
      logs.push('Fetching marketplace object...');
      const marketplaceObj = await suiClient.getObject({
        id: MARKETPLACE_ID,
        options: {
          showContent: true,
          showType: true,
        },
      });

      logs.push('Marketplace object:');
      logs.push(JSON.stringify(marketplaceObj, null, 2));
      logs.push('');

      // Extract table IDs
      const content = marketplaceObj.data?.content;
      if (content && content.dataType === 'moveObject') {
        const fields = content.fields as any;
        logs.push(`Items table ID: ${fields.items?.fields?.id?.id}`);
        logs.push(`Item counter: ${fields.item_counter}`);
        logs.push(`Offer counter: ${fields.offer_counter}`);
        logs.push(`Escrow counter: ${fields.escrow_counter}`);
        logs.push('');

        // Try to get dynamic fields from items table
        const itemsTableId = fields.items?.fields?.id?.id;
        if (itemsTableId) {
          logs.push('Fetching dynamic fields from items table...');
          const dynamicFields = await suiClient.getDynamicFields({
            parentId: itemsTableId,
            limit: 50,
          });

          logs.push(`Found ${dynamicFields.data.length} dynamic fields in items table`);
          logs.push('');

          // Fetch and display each item
          for (let i = 0; i < dynamicFields.data.length; i++) {
            const field = dynamicFields.data[i];
            logs.push(`--- Item ${i + 1} ---`);
            logs.push(`Dynamic Field Object ID: ${field.objectId}`);
            logs.push(`Name Type: ${field.name.type}`);
            logs.push(`Name Value (Item ID): ${field.name.value}`);

            try {
              const itemObj = await suiClient.getDynamicFieldObject({
                parentId: itemsTableId,
                name: field.name,
              });

              if (itemObj.data?.content && itemObj.data.content.dataType === 'moveObject') {
                const itemFields = (itemObj.data.content.fields as any).value?.fields;
                logs.push(`Title: ${itemFields.title}`);
                logs.push(`Price: ${itemFields.price} MIST`);
                logs.push(`Seller: ${itemFields.seller}`);
                logs.push(`Status: ${itemFields.status}`);
                logs.push(`Created: ${new Date(parseInt(itemFields.created_at)).toLocaleString()}`);
              }
            } catch (err) {
              logs.push(`Error fetching item: ${err}`);
            }
            logs.push('');
          }
        }
      }

      setOutput(logs.join('\n'));
    } catch (error) {
      setOutput(`Error: ${error instanceof Error ? error.message : String(error)}\n\nStack: ${error instanceof Error ? error.stack : ''}`);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="min-h-screen p-8">
      <div className="max-w-4xl mx-auto">
        <h1 className="text-3xl font-bold mb-6">🔍 Marketplace Debug Tool</h1>

        <button
          onClick={checkMarketplace}
          disabled={loading}
          className="retro-btn px-6 py-3 mb-6"
        >
          {loading ? 'Loading...' : 'Check Marketplace State'}
        </button>

        {output && (
          <pre className="retro-card p-4 overflow-auto text-xs whitespace-pre-wrap">
            {output}
          </pre>
        )}
      </div>
    </div>
  );
}

```

### frontend/src/app/listings/page.tsx

```typescript
"use client"

import { Suspense, useState, useEffect } from "react"
import { useSearchParams, useRouter } from "next/navigation"
import { ItemCard, ItemCardSkeleton } from "@/components/ItemCard"
import { getAllItems, getItemsByIds } from "@/lib/sui/queries"
import { ItemStatus } from "@/lib/types/sui-objects"
import type { ItemCardProps } from "@/components/ItemCard"

/**
 * Listings Page - Browse all marketplace items with AI-powered search
 *
 * Features:
 * - Browse all items (no query)
 * - AI semantic search (with ?q= query parameter)
 * - Responsive grid layout
 * - Loading states with skeleton loaders
 * - Empty state handling
 */
function ListingsContent() {
  const searchParams = useSearchParams()
  const router = useRouter()
  const [items, setItems] = useState<ItemCardProps[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [searchQuery, setSearchQuery] = useState(searchParams.get('q') || '')

  useEffect(() => {
    const query = searchParams.get('q')
    setSearchQuery(query || '')
    loadItems(query || '')
  }, [searchParams])

  const loadItems = async (query: string) => {
    setLoading(true)
    setError(null)

    try {
      let activeItems

      if (query.trim()) {
        // AI Search mode
        console.log(`🔍 AI Search: "${query}"`)

        // Call AI search API
        const searchResponse = await fetch('/api/ai/search', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            query,
            similarityThreshold: 0.3,  // Lower threshold for better recall
            maxResults: 50,
            useCombined: true,
          }),
        })

        if (!searchResponse.ok) {
          throw new Error('Search failed')
        }

        const searchResult = await searchResponse.json()
        console.log(`✓ Found ${searchResult.count} matching items`)

        if (searchResult.results.length === 0) {
          setItems([])
          setLoading(false)
          return
        }

        // Fetch items from blockchain using search results
        const itemsResponse = await getItemsByIds(searchResult.results)
        activeItems = itemsResponse.filter(item => item && item.fields.status === ItemStatus.Active)
      } else {
        // Browse all mode
        console.log('📋 Browsing all items')
        const response = await getAllItems(undefined, { limit: 100 })
        activeItems = response.data.filter(item => item.fields.status === ItemStatus.Active)
      }

      const mapped: ItemCardProps[] = activeItems.map(item => ({
        objectId: item.objectId,
        title: item.fields.title,
        priceMist: BigInt(item.fields.price),
        category: item.fields.category,
        walrusImageIds: item.fields.walrus_image_ids || [],
        seller: item.fields.seller,
      }))

      setItems(mapped)
    } catch (err) {
      console.error('Error loading items:', err)
      setError('Failed to load listings. Please try again.')
    } finally {
      setLoading(false)
    }
  }

  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault()
    const query = searchQuery.trim()
    if (query) {
      router.push(`/listings?q=${encodeURIComponent(query)}`)
    } else {
      router.push('/listings')
    }
  }

  return (
    <div className="min-h-screen">
      <div className="container mx-auto px-4 py-8">
        {/* Header */}
        <div className="retro-card retro-shadow p-6 mb-8">
          <h1 className="text-4xl font-black mb-2">
            {searchParams.get('q') ? 'Search Results' : 'Browse Listings'}
          </h1>
          <p className="text-lg opacity-80">
            {searchParams.get('q')
              ? `AI-powered semantic search for: "${searchParams.get('q')}"`
              : 'Discover unique thrifted items on the blockchain'}
          </p>

          {/* Stats */}
          {!loading && items.length > 0 && (
            <div className="mt-4 pt-4 border-t-2 border-black border-dashed">
              <div className="flex gap-6 text-sm">
                <div>
                  <span className="font-bold">{items.length}</span>
                  <span className="opacity-60 ml-1">{searchParams.get('q') ? 'results found' : 'items listed'}</span>
                </div>
              </div>
            </div>
          )}
        </div>

        {/* Search Bar */}
        <div className="retro-card retro-shadow p-4 mb-8">
          <form onSubmit={handleSearch} className="flex gap-3">
            <input
              type="text"
              value={searchQuery}
              onChange={(e) => setSearchQuery(e.target.value)}
              placeholder="Search by meaning: 'vintage leather jacket', 'warm winter coat'..."
              className="flex-1 px-4 py-2 border-2 border-black retro-card outline-none focus:shadow-[2px_2px_0px_rgba(0,0,0,1)]"
            />
            <button
              type="submit"
              className="px-6 py-2 bg-black text-white retro-btn"
            >
              Search
            </button>
            {searchParams.get('q') && (
              <button
                type="button"
                onClick={() => {
                  setSearchQuery('')
                  router.push('/listings')
                }}
                className="px-4 py-2 border-2 border-black retro-btn"
              >
                Clear
              </button>
            )}
          </form>
        </div>

        {/* Loading State */}
        {loading && (
          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
            {Array.from({ length: 8 }).map((_, i) => (
              <ItemCardSkeleton key={i} />
            ))}
          </div>
        )}

        {/* Error State */}
        {error && !loading && (
          <div className="retro-card retro-shadow p-6 bg-red-50 border-2 border-red-500 text-center">
            <div className="t
[truncated — 3029 more characters]
```

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