A privacy-first job network for verifiable web data collection
The Problem We're Solving
The modern data economy runs on web data that lives behind logins, paywalls, and account gates—think Crunchbase company profiles, LinkedIn insights, SaaS analytics dashboards, or proprietary B2B databases. This data is incredibly valuable for investors, growth teams, AI training pipelines, and market intelligence platforms.
But accessing it programmatically is a nightmare:
Our Solution: A Decentralized Data Access Layer with Cryptographic Proofs
gh0st.market creates a two-sided marketplace where data requesters connect with authorized operators (humans or AI agents) who already have legitimate access to target platforms. The magic? Every data delivery is accompanied by a zk-TLS proof—cryptographic evidence that the data genuinely came from the claimed source, without revealing credentials or session details.
How It Works
Then they create individual Jobs referencing that spec, with concrete inputs (e.g., {slug: "anthropic"}) and an escrowed bounty in ETH or any ERC-20 token. The bounty is locked in the smart contract until work is verified.
The gh0st browser extension acts as their AI-powered work environment:
No middleman. No disputes. No trust required.
Technical Architecture
Smart Contracts (Solidity + Foundry)
Web Application (Next.js 16 + React 19)
Browser Extension (Plasmo + TypeScript)
zk-TLS Integration (vlayer)
The cryptographic backbone that makes this trustless:
Why This Matters
For AI Agent Builders
gh0st.market is infrastructure for the agentic web. AI agents need real-time data from authenticated sources, but they can't hold credentials safely or prove their outputs are genuine. With gh0st, agents can:
For Data Teams
Stop maintaining brittle scraping infrastructure. Instead:
For Workers & Operators
Monetize access you already have:
What Makes This a Strong Hackathon Project
Full-Stack Implementation This isn't a mockup—it's a working system spanning smart contracts, a production-quality web app, and a browser extension with real state management.
Novel Architecture Combining zk-TLS proofs with a job marketplace is genuinely new. We're not just "blockchain + scraping"—we're creating verifiable data infrastructure.
Real Market Need The web data market is $5B+ and growing. Every AI company, hedge fund, and growth team struggles with this problem. We're building picks and shovels for the AI gold rush.
Privacy-First Design Both sides stay pseudonymous. Requesters don't reveal what data they're collecting at scale; workers don't reveal their credentials. Only proofs and payments hit the chain.
Extensible Foundation The Job Spec system is a protocol primitive. Anyone can create specs for any website. The ecosystem grows organically as workers approve new domains.
AI-Native Built from day one for AI agents to participate—as requesters posting jobs or as workers executing them autonomously.
The Vision
gh0st.market is the HTTP of authenticated web data. Just as APIs standardized how services talk to each other, we're standardizing how AI agents and applications access human-permissioned web data with cryptographic trust.
Imagine:
We're not building a scraping tool. We're building the trust layer for the web data economy.
How We Built gh0st.market: The Technical Deep Dive
Architecture Overview
gh0st.market is a three-part system that had to work together seamlessly: smart contracts handling escrow and verification, a web application for both requesters and workers, and a browser extension that actually executes jobs and generates proofs. Getting these pieces to communicate reliably was the core engineering challenge.
Smart Contracts: Foundry + Solidity
We chose Foundry over Hardhat for its speed and native Solidity testing. The contract architecture centers on two key abstractions:
The JobSpec / Job Pattern
Rather than having requesters define everything per-job, we separated templates (JobSpecs) from instances (Jobs):
struct JobSpec { string mainDomain; // "crunchbase.com" string notarizeUrl; // "https://crunchbase.com/organization/{{orgSlug}}" string promptInstructions; // AI extraction instructions string outputSchema; // Expected JSON schema string inputSchema; // Placeholder types address creator; bool active; }
struct Job { uint256 specId; // Reference to template string inputs; // Concrete values: {"orgSlug": "anthropic"} address token; // ETH (address(0)) or ERC-20 uint256 bounty; JobStatus status; string resultPayload; address worker; }
This means anyone can create a reusable spec for a domain, and the ecosystem benefits from shared templates. Workers approve specs once, then see all matching jobs automatically.
Multi-Token Escrow
We wanted to support both native ETH and stablecoins (USDC) from day one:
function createJob(CreateJobParams calldata params) external payable { if (params.token == address(0)) { // Native ETH - must match msg.value if (msg.value != params.bounty) revert InvalidBounty(); } else { // ERC-20 - pull tokens via transferFrom if (msg.value != 0) revert TokenMismatch(); IERC20(params.token).safeTransferFrom(msg.sender, address(this), params.bounty); } // ... create job }
On payout, the same logic reverses—ETH via call{value} or ERC-20 via safeTransfer. We use OpenZeppelin's SafeERC20 and ReentrancyGuard to prevent the obvious attack vectors.
The Proof Verifier Interface
The contract calls an external IProofVerifier to validate zk-TLS proofs:
interface IProofVerifier { function verifyProof( bytes calldata proof, string calldata targetDomain ) external view returns (bool valid); }
Hacky but necessary: For the hackathon, our ProofVerifier.sol is a mock that always returns true. The interface is production-ready for vlayer integration—we just swap the implementation address. This let us build the full flow without blocking on proof generation complexity.
Batch Query Optimization
Frontends need to display lists of specs and jobs. Rather than N+1 RPC calls, we added range queries:
function getJobSpecsRange(uint256 from, uint256 to) external view returns (JobSpec[] memory specs) { uint256 length = to - from; specs = new JobSpec; for (uint256 i = 0; i < length; i++) { specs[i] = _specs[from + i]; } }
Two RPC calls (get count, then get range) instead of potentially hundreds.
Web Application: Next.js 16 + React 19 + wagmi v3
Why This Stack
wagmi CLI for Type Generation
This was a huge DX win. We use wagmi generate to produce fully-typed hooks from our contract ABIs:
// wagmi.config.ts export default defineConfig({ out: 'src/generated.ts', contracts: [ { name: 'JobRegistry', abi: jobRegistryAbi, }, ], });
Now useReadContract and useWriteContract have full TypeScript inference for function names, argument types, and return types. No more ABI typos at runtime.
Event-Based Data Fetching
Here's where it gets interesting. We needed to show "all jobs created by this user" but the contract only stores jobs by ID, not by creator. Solution: query events.
export function useUserJobs(userAddress: 0x${string} | undefined) {
const publicClient = usePublicClient();
return useQuery({
queryKey: ["userJobs", userAddress],
queryFn: async () => {
// Get all JobCreated events filtered by requester
const logs = await publicClient.getLogs({
address: JOB_REGISTRY_ADDRESS,
event: parseAbiItem(
"event JobCreated(uint256 indexed jobId, uint256 indexed specId, address indexed requester, address token, uint256 bounty)"
),
args: { requester: userAddress },
fromBlock: DEPLOYMENT_BLOCK, // Skip genesis blocks
toBlock: "latest",
});
// Fetch full job details for each
return Promise.all(logs.map(async (log) => {
const job = await publicClient.readContract({
address: JOB_REGISTRY_ADDRESS,
abi: jobRegistryAbi,
functionName: "getJob",
args: [log.args.jobId],
});
return { ...job, id: log.args.jobId };
}));
},
});
}
We store DEPLOYMENT_BLOCK in a generated config file so we don't scan from block 0 on Sepolia (which would timeout).
Dynamic Labs Integration
Dynamic gives us wallet connection with a much better UX than raw RainbowKit:
export function Web3Provider({ children }: { children: React.ReactNode }) { return ( <DynamicContextProvider settings={{ environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!, walletConnectors: [EthereumWalletConnectors], }} > <DynamicWagmiConnector> <WagmiProvider config={wagmiConfig}> <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> </WagmiProvider> </DynamicWagmiConnector> </DynamicContextProvider> ); }
Users can connect with MetaMask, WalletConnect, or even email—Dynamic handles the embedded wallet creation. This dramatically lowers the barrier for non-crypto-native users.
Dual-Role Architecture
The same app serves requesters and workers. We handle this with a role toggle that preserves navigation context:
function getEquivalentPath(currentPath: string, newRole: Role): string { // /requestor/jobSpecs/123/jobs → /worker/jobSpecs/123/jobs const segments = currentPath.split('/'); segments[1] = newRole; return segments.join('/'); }
Both roles see different sidebars and slightly different UIs, but share components like JobSpecCard and DashboardLayout.
Browser Extension: Plasmo + Worker Engine Architecture
This is where most of the complexity lives. The extension needs to:
Why Plasmo
Plasmo is a framework for building browser extensions with React. It handles:
We can write the popup as a React component and Plasmo handles the Chrome extension boilerplate.
Local Database with Drizzle ORM
Workers need persistent state that survives browser restarts. We use Drizzle ORM with SQLite (via sql.js compiled to WASM):
// db/schema.ts export const followedSpecs = sqliteTable("followed_specs", { id: integer("id").primaryKey({ autoIncrement: true }), specId: integer("spec_id").notNull(), walletAddress: text("wallet_address").notNull(), mainDomain: text("main_domain").notNull(), minBounty: real("min_bounty").default(0), autoClaim: integer("auto_claim", { mode: "boolean" }).default(false), });
export const activeJobs = sqliteTable("active_jobs", { jobId: text("job_id").notNull().unique(), status: text("status", { enum: ["pending", "navigating", "collecting", "generating_proof", "submitting", "completed", "failed"], }).notNull().default("pending"), progress: integer("progress").default(0), // ... });
This gives us type-safe queries and migrations without a server.
The Worker Engine State Machine
The core of the extension is workerEngine.ts—a state machine that orchestrates everything:
export interface WorkerEngine { start(): void; // Begin listening for jobs stop(): void; // Pause everything openWorkerTab(): Promise<number>; // Create dedicated execution tab setApprovedSpecs(specIds: Set<number>, minBountyBySpec: Map<number, number>): void; setAutoMode(enabled: boolean); // Auto-process queue processNextJob(): Promise<JobResult | null>; // Manual trigger getStatus(): WorkerStatus;
// Event subscriptions
onStatusChange(cb: (status: WorkerStatus) => void): () => void;
onProgress(cb: (progress: JobProgress) => void): () => void;
onJobComplete(cb: (result: JobResult) => void): () => void;
}
The engine coordinates three sub-modules:
The Worker Tab Pattern: Jobs execute in a dedicated browser tab (/worker/runner), not in a headless context. This is intentional—it uses the worker's real browser profile with real cookies and sessions. The extension controls this tab via chrome.tabs APIs, navigating it to target URLs and extracting data.
Web ↔ Extension Communication
The web app needs to know if the extension is installed, query worker preferences, and receive job progress updates. We built a typed message protocol:
// Message types with GH0ST_ prefix for namespacing export type WebToExtensionMessage = | { type: "GH0ST_PING" } | { type: "GH0ST_START_JOB"; payload: StartJobPayload } | { type: "GH0ST_QUERY"; payload: QueryPayload } | { type: "GH0ST_FOLLOW_SPEC"; payload: FollowSpecPayload };
export type ExtensionToWebMessage = | { type: "GH0ST_PONG"; payload: { version: string } } | { type: "GH0ST_JOB_PROGRESS"; payload: JobProgressPayload } | { type: "GH0ST_JOB_COMPLETED"; payload: JobCompletedPayload };
Communication flows through a content script injected into the web app:
Web App → window.postMessage → Content Script → chrome.runtime.sendMessage → Background Script Background Script → chrome.tabs.sendMessage → Content Script → window.postMessage → Web App
Hacky detail: We track which tabs have the web app open (connectedTabs Set) so we can broadcast progress updates to all of them. When a tab closes, we clean it up via chrome.tabs.onRemoved.
vlayer Client Abstraction
For zk-TLS proof generation, we abstracted behind an interface:
export interface IVlayerClient { generateProof(request: ProofRequest): Promise<ProofResult>; verifyProof(proof: string, domain: string): Promise<boolean>; }
export function createVlayerClient(): IVlayerClient { const useMock = process.env.PLASMO_PUBLIC_USE_MOCK === 'true';
if (useMock) {
return new MockVlayerClient(); // Returns fake proofs instantly
}
return new VlayerClient({ clientId, secret }); // Real vlayer integration
}
The mock client lets us develop the full flow without vlayer credentials. In production, we swap to the real client—same interface, real proofs.
Notable Hacks & Clever Solutions
Deployment addresses change between local Anvil and Sepolia. We generate a config file at deploy time:
// Generated by deploy script export const JOB_REGISTRY_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; export const DEPLOYMENT_BLOCK = 12345678n; export const CHAIN_ID = 11155111;
The web app imports this, so switching networks is just a redeploy + regenerate.
Testing extension features without building/installing the extension constantly:
// In useExtensionStatus hook if (localStorage.getItem("gh0st_extension_mock") === "true") { return { connected: true, version: "dev", activeTask: { jobId: "0x...", status: "collecting", progress: 45 }, }; }
Set a localStorage flag and the web app pretends the extension is connected with an active task.
When a user creates a job spec, we don't wait for blockchain confirmation to update the UI. We show a toast, then refetch via events once confirmed:
useEffect(() => { if (isSpecCreated) { refetchSpecs(); // Re-query events to get the new spec setIsCreateSpecModalOpen(false); } }, [isSpecCreated, refetchSpecs]);
The popup has two modes: setup (first run) and operational. We persist config in chrome.storage.local:
export async function saveConfig(config: ExtensionConfig): Promise<void> { await chrome.storage.local.set({ gh0st_config: config }); }
export async function hasConfig(): Promise<boolean> { const result = await chrome.storage.local.get("gh0st_config"); return !!result.gh0st_config; }
On first open, workers enter their RPC URL, contract address, and a private key for signing submissions. The key never leaves local storage.
The extension polls for new jobs but needs to avoid re-queueing jobs it's already seen:
// In jobListener.ts const seenJobIds = new Set<string>();
function onNewJob(job: Job) { const jobKey = job.id.toString(); if (seenJobIds.has(jobKey)) return; seenJobIds.add(jobKey);
// Check against approved specs and min bounty
if (!approvedSpecIds.has(Number(job.specId))) return;
const minBounty = minBountyBySpec.get(Number(job.specId)) || 0;
if (parseFloat(formatEther(job.bounty)) < minBounty) return;
onJobFound(job);
}
Partner Technologies
| Technology | How It Helped | |--------------|---------------------------------------------------------------------------------------------------| | Dynamic Labs | Wallet connection with email/social fallback—critical for onboarding non-crypto users | | vlayer | zk-TLS proof infrastructure—the cryptographic core that makes trustless verification possible | | Foundry | Fast Solidity compilation and testing; forge test runs our full suite in ~2 seconds | | Plasmo | Made browser extension development feel like building a React app instead of fighting Chrome APIs | | wagmi v3 | Type-safe contract hooks with TanStack Query integration—caught multiple bugs at compile time |
What We'd Do Differently
Lines of Code
All written in 48 hours. We're proud of how complete the system is—not just a demo, but a working protocol with real escrow, real payments, and a real browser automation pipeline.

