Search projects, hackathons, sponsors and tech

    ← All projects

    ghost-work

    Your rate is nobody's business

    Demo

    About

    The problem it solves Freelancing is broken in a very specific way that nobody talks about: **everyone can see everyone else's deal**. When you hire a freelancer through an on-chain contract today whether it's on Ethereum, Solana, or any public L1, the entire world can see what you're paying them. Your competitor can look up your dev budget. Other freelancers on the same project can see what each other earns. An auditor reviewing your books gets full access to every individual payment, name, and rate. This isn't a theoretical problem. It's why most serious freelance payments still happen through bank wires and PDFs because the moment you put a contractor agreement on a public blockchain, you've published salary data to the world. **GhostWork fixes this using Canton's sub-transaction privacy.** Not with access control lists. Not with encryption wrappers. At the *protocol level*, each participant's node physically receives only the data they're authorized to see. Here's what that actually looks like in practice: - **The Client (Ethereum Foundation)** sees all their contracts and payment records, they're a signatory on everything they created - **Freelancer Nidhi** sees only her own contract and payments. She has zero knowledge that Akash even exists on the platform, his data was never sent to her participant node - **Freelancer Akash** sees only his own contract. Nidhi's rates, project descriptions, and payment history are physically absent from his node - **Auditor Eve** sees aggregate totals only, "2 contracts, $7,000 total paid" but zero individual details. No names, no rates, no project descriptions This isn't filtering. It's not hiding rows in a database. The data literally does not exist on nodes that shouldn't have it. That's the difference between access control and actual privacy. ### What People Can Use It For - **Freelance platforms** that want to stop rate-comparison shopping between contractors - **Enterprise contractor management** where different departments shouldn't see each other's vendor deals - **Compliance auditing** where auditors need to verify totals without seeing individual records (think SOC 2 aggregate reporting) - **Any B2B payment workflow** where counterparty confidentiality matters be it invoice financing, consulting fees, advisory retainers The guided demo walks you through the full lifecycle in about 5 minutes: send a proposal, accept it, submit milestones, approve payments, generate an audit, switching between accounts at each step to prove what each party can and cannot see. Use of AI tools and agents I used **Claude Code** as a pair programming partner throughout the build. Here's how: **What Claude handled well:** - Scaffolding React components from rough descriptions, I'd describe the UI I wanted and it would generate the JSX, then I'd tweak styling and logic - Refactoring across multiple files simultaneously, when I changed the Daml contract to add **paymentTimestamp**, Claude updated the test file, the API client, and the frontend store in one pass - Catching type errors before build, it would flag missing fields in TypeScript interfaces before I even ran **tsc** **What I had to do myself:** - All Daml contract design decisions, the privacy model, signatory/observer assignments, and the propose-accept pattern were my architectural choices - Canton API integration, Claude doesn't know the JSON Ledger API v2 response format, so I had to debug the actual HTTP responses and teach it the nesting structure - DevNet deployment, SSH tunnels, JWT configuration, validator onboarding this was all manual infrastructure work - UX decisions which party sees what, the demo guide flow, the "not connected" landing page design **The workflow was roughly:** I'd design the approach, write the critical integration code, then use Claude to parallelize the UI implementation across multiple files. It saved significant time on repetitive frontend tasks, but every Daml contract line and every Canton API call was hand-verified against the actual ledger responses.

    How it's made

    Challenges we ran into ### 1. ARM64 on macOS I'm building on an M1 Mac. The Canton/Daml SDK ships Linux x86_64 binaries. The cn-quickstart Docker image is Ubuntu-based. Getting all three to play nice together cost me almost a full day. The fix was building a custom Docker dev image (cantonlance-dev) with JDK 21, Node.js 20, and the ARM64 Daml SDK (daml-sdk-3.4.10-linux-aarch64.tar.gz). Once I had that, everything ran natively without emulation. But the initial repo setup docs assume x86_64, and there's zero mention of ARM anywhere. ### 2. The Canton JSON Ledger API v2 Response Format The documentation for the JSON Ledger API v2 is... sparse. I spent hours figuring out that `POST /v2/state/active-contracts` returns a nested structure like: ```json { "contractEntry": { "JsActiveContract": { "createdEvent": { "templateId": "packageId:Module:Template", "createArgument": { ... }, "contractId": "..." } } } } ``` There's no example of this in the docs. I had to reverse-engineer it by logging raw responses and walking through the nesting. ### 3. Daml's Signatory Model Coming from Solidity, my first instinct was to have the client create a `ProjectContract` directly with both parties as signatories. Daml said no — you can't create a contract that requires someone else's signature without their consent. This forced me into the propose-accept pattern, which is actually a much better design ```haskell -- Client creates a proposal (only client is signatory) template ProjectProposal with client : Party freelancer : Party ... where signatory client observer freelancer choice AcceptProposal : ContractId ProjectContract controller freelancer -- freelancer must consent do create ProjectContract with ... -- now both are signatories ``` What felt like a limitation turned out to be the right abstraction. Proposals show up in the freelancer's "Incoming Proposals" and they explicitly accept or decline. Much more honest than auto-enrolling someone in a contract. ### 4. DevNet Sequencer Outage I had the app working on local sandbox and tried to deploy to Canton DevNet. Got the validator running, onboarded to the network, set up JWT auth, everything looked good. Then the DevNet sequencers went down. The validator couldn't register the synchronizer domain, so no transactions could be submitted. I pivoted to making the local sandbox experience bulletproof instead. The app detects which environments are available on startup and lets you switch between them. The DevNet code path still exists and works, it's just waiting for the sequencers to come back. ### 5. Sandbox Auth: The Token Header That Wasn't Canton's local sandbox mode doesn't need authentication. Sounds simple. But if you send an `Authorization` header with an empty token, the Pekko HTTP server throws `Illegal header` warnings into the logs constantly. The fix was conditionally skipping the header entirely in sandbox mode: ```typescript if (this.config.mode === "local") { // Sandbox doesn't require auth — skip Authorization header to avoid // Pekko HTTP "Illegal header" warnings from colons in token } else { headers["Authorization"] = `Bearer ${partyConfig.token}`; } ``` Small thing, but it took a while to figure out why the logs were full of warnings.