👗 Fashion & Textile Design · supply chain transparency

Designer Direct

Connect designers directly with suppliers using onchain verified transactions and profiles.

Privy social + sponsored tx· wallet UX
Section · Onchain

The primitive.

full primer →

Fashion designers sign in with Google through Privy — no seed phrase, no MetaMask popup — and their supply chain transparency actions are sent as sponsored transactions on World Chain so they never see gas.

Why this primitivePrivy social login and gasless tx simplify supply chain verification and reduce intermediaries.

Kernel
Privy embedded wallet bootstrapped with PRIVY_APP_ID — Google sign-in plus native sponsored transactions on World Chain Sepolia so users never see gas
Drives the UI as
a one-click 'Sign in with Google' that drops the user straight into the app with an onchain identity
Appendix · Secrets

Required keys.

METAMASK_PRIVATE_KEY
Exported from MetaMask. Fund on World Chain Sepolia via the Alchemy faucet.
open ↗
SEPOLIA_RPC_URL
Alchemy World Chain Sepolia HTTPS endpoint. Create a free app → copy the HTTPS URL.
open ↗
ETHERSCAN_API_KEY
Required for npx hardhat verify after deploy.
open ↗
PRIVY_APP_ID
Enables Google sign-in and sponsored transactions.
open ↗
PINATA_JWT
Pins images / JSON / manifests to IPFS.
open ↗

Add these in your Lovable project under Settings → Secrets before pasting the prompt below.

Appendix · Mega-prompt

The build prompt.

Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →

Build "Designer Direct" in ONE Lovable message. Single-page demo.

CONCEPT
Connect designers directly with suppliers using onchain verified transactions and profiles.
Discipline: Fashion & Textile Design (supply chain transparency).
Onchain primitive: Privy social + sponsored tx. Why this primitive: Privy social login and gasless tx simplify supply chain verification and reduce intermediaries.

5-CREDIT BUDGET (HARD LIMIT):
- ONE single-page app. No router, no Lovable Cloud, no database, no auth flows beyond Privy drop-in.
- ONE Solidity contract, <=80 lines, deployed to World Chain Sepolia, verified on Worldscan.
- Privy is always the auth + sponsored-tx layer (Google login, embedded wallet).
- Pinata/IPFS only if the idea genuinely needs to store a file or metadata.
- At most ONE AI call per user action (use Lovable AI Gateway with LOVABLE_API_KEY if AI is part of the idea).
- Skip tests, skip CI, skip docs pages. Ship the demo, nothing else.

STACK
- React + Vite single page (the index route).
- SSR-safe Privy mount is mandatory. Never import @privy-io/react-auth at
  module scope of a route file — it crashes SSR. Use
  lazy(() => import('./privy-client-entry')) inside <ClientOnly> + <Suspense>,
  and put <PrivyProvider> only inside privy-client-entry.tsx.
- PrivyProvider config (do NOT stub defaultChain as { id, name } — omit it
  or pass viem's `sepolia`; chainId is passed per-call):
    <PrivyProvider appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{ loginMethods:['google','email'],
                embeddedWallets:{ ethereum:{ createOnLogin:'users-without-wallets' } },
                appearance:{ theme:'dark' } }}>
- Read the embedded wallet from useWallets, not user.wallet:
    const embedded = wallets.find(w => w.walletClientType === 'privy');
- Every send goes through Privy `useSendTransaction` with BOTH `address`
  and `sponsor`, wrapped in a 45s Promise.race timeout whose reject message
  names the exact dashboard toggles:
    await Promise.race([
      sendTransaction(
        { to, data, chainId: 4801 },
        { address: embedded.address, sponsor: true }
      ),
      new Promise((_, r) => setTimeout(() => r(new Error(
        "Privy sendTransaction timed out after 45s. Enable Gas sponsorship -> App pays -> World Chain Sepolia -> Allow transactions from the client."
      )), 45_000)),
    ]);
- Do NOT pass uiOptions:{ showWalletUIs:false } — it aborts with
  "signal is aborted without reason". The approval sheet still shows on
  the embedded-EOA path; the fee reads US$0.00.
- Do NOT add ZeroDev / SmartWalletsProvider / a paymaster URL.
- World ID v4 humanness gate is mandatory before any onchain write.
  Production `app_id`s no longer accept the classic `worldapp://` WalletConnect
  deep link, so the v2 `IDKitWidget` "Open World App" button silently does
  nothing on real devices. Use v4: `IDKitRequestWidget` + server-signed
  `rp_context` + a server-side verify proxy. Modern World ID 4.0 apps are
  Orb-backed — you MUST use `orbLegacy({ signal })` as the preset;
  `deviceLegacy` triggers `failed_by_host_app` and `handleVerify` never fires.
  PORTAL SETUP (do this in https://developer.world.org before coding):
    1. Create/open the app -> copy `app_id` (public, ships to browser).
    2. Actions tab -> create an action slug (e.g. `log-action`). Verification
       level: Orb (World ID 4.0 default).
    3. Relying Parties tab (or "World ID 4.0" tab) -> create an RP. Copy the
       `rp_id` (public, `rp_...`) AND the signing key (hex private key, shown
       ONCE).
    4. Allow legacy proofs: World ID 4.0 apps have this implicitly enabled;
       the widget still needs `allow_legacy_proofs={true}`. Only revisit if
       verify returns `invalid_proof` while the client flag is set.
  PACKAGE: `bun add @worldcoin/idkit` (v4+; re-exports
  `@worldcoin/idkit-core/signing`). Do NOT install v2.
  FILE LAYOUT (5 files):
    - src/config/worldid.ts — PUBLIC ONLY. `{ appId: 'app_...' as `app_${string}`,
      action: 'log-action' }`. Never put rp_id or signing key here; anything
      under src/config/* ships to the browser.
    - src/components/worldid-gate.tsx — trigger button + `open` state. Lazy-load
      the inner widget:
        const IDKitInner = lazy(() => import('./worldid-inner'));
        // <ClientOnly fallback={null}><Suspense><IDKitInner open={open} .../></Suspense></ClientOnly>
    - src/components/worldid-inner.tsx — the actual widget. FREEZE the wallet
      address into a ref/state the moment the widget opens; use that frozen
      value in `orbLegacy({ signal })` AND in the verify POST body. Privy/wagmi
      can re-hydrate mid-flow and mutate `signal`, producing a valid-looking
      proof that server-verifies against the wrong signal.
      On `open` -> POST `/api/public/idkit/rp-signature` with `{ action }`,
      store the returned `{ rp_id, nonce, created_at, expires_at, signature }`
      as `rp_context`, then render:
        import { IDKitRequestWidget, orbLegacy } from '@worldcoin/idkit';
        const frozenSignal = useRef(''); // set in useEffect when open becomes true
        <IDKitRequestWidget open={open} onOpenChange={onOpenChange}
          app_id={appId} action={action} rp_context={rp}
          allow_legacy_proofs environment="production"
          preset={orbLegacy({ signal: frozenSignal.current })}
          handleVerify={async (result) => {
            const res = await fetch('/api/public/idkit/verify', {
              method:'POST', headers:{'Content-Type':'application/json'},
              body: JSON.stringify({
                rp_id: rp.rp_id, app_id: appId, idkitResponse: result,
              }),
            });
            if (!res.ok) {
              const j = await res.json().catch(() => ({}));
              throw new Error(`verify ${res.status}: ${j.detail ?? j.code}`);
            }
          }}
          onSuccess={onVerified} />
    - src/routes/api/public/idkit/rp-signature.ts — TSS server route. Reads
      `process.env.WORLDID_RP_ID` and `process.env.WORLDID_RP_SIGNING_KEY`
      INSIDE the handler (never at module scope), then:
        import { signRequest } from '@worldcoin/idkit-core/signing';
        const sig = signRequest({ signingKeyHex: signingKey, action, ttl: 300 });
        return Response.json({ rp_id: rpId, nonce: sig.nonce,
          created_at: sig.createdAt, expires_at: sig.expiresAt,
          signature: sig.sig });
    - src/routes/api/public/idkit/verify.ts — TSS server route. Reads the
      POST body `{ rp_id, app_id, idkitResponse }`, then UNWRAPS and forwards
      `idkitResponse` (top-level proof fields — nullifier_hash, merkle_root,
      proof, verification_level, action, signal_hash) to the CURRENT v4 host
      `https://developer.world.org/api/v4/verify/${rp_id}` with explicit
      Accept + User-Agent headers (the World edge returns text/html 403s
      without them). On a non-JSON 403 from the rp_id path, retry ONCE using
      `app_id` before giving up. Return diagnostic metadata (upstreamStatus,
      upstreamContentType, verifyIdSource, fallbackTried) so the UI can show
      a safe debug blob:
        const payload = body.idkitResponse ?? body;
        async function post(id) {
          const url = `https://developer.world.org/api/v4/verify/${id}`;
          const r = await fetch(url, {
            method:'POST',
            headers:{ 'Accept':'application/json',
                      'Content-Type':'application/json',
                      'User-Agent':'yourapp-worldid/1.0' },
            body: JSON.stringify(payload),
          });
          const text = await r.text();
          const ct = r.headers.get('content-type') ?? '';
          let parsed = null; try { parsed = JSON.parse(text); } catch {}
          return { r, text, ct, parsed, isJson: !!parsed || ct.includes('json') };
        }
        let a = await post(rpId); let fallbackTried = false;
        if (a.r.status === 403 && !a.isJson && appId && appId !== rpId) {
          fallbackTried = true;
          const b = await post(appId); if (b.r.ok) a = b;
        }
      Wrapping the proof under `{ rp_id, idkitResponse }` in the upstream POST
      returns `invalid_proof` immediately after World App shows "successful".
  UI RULES (avoid the "same issue" loop):
    * Separate "proof received" from "server verified" state. On backend
      failure, KEEP the proof-received state — do NOT bounce the user back to
      "Connect World App". Show the server error inline.
    * Add a "Copy debug" button that dumps a SAFE non-secret JSON blob:
      { appId (prefix...suffix), action, rpId (prefix...suffix),
        protocolVersion, environment, responseIdentifiers, upstreamStatus,
        upstreamHost, upstreamContentType, fallbackTried, verifyIdSource }.
    * On success show an explicit "Verified human" line; collapse the debug
      JSON behind a <details>. If the debug shows upstreamStatus 200 +
      application/json + verifyIdSource "body.rp_id" the flow IS working —
      the UI just needs to celebrate it.
  RULES:
    * `/api/public/*` prefix is mandatory — bypasses Lovable's published-site
      auth so World App can reach the endpoints cross-origin.
    * `<ClientOnly>` + lazy import is mandatory — IDKit references `window` at
      import time; a top-level import in a route file crashes SSR with
      `window is not defined`.
    * Fetch a fresh rp_signature on every `open` (5-min TTL); caching across
      the widget lifetime causes stale-nonce failures.
    * Read `process.env.*` INSIDE handlers, never at module scope of route
      files — Cloudflare Workers inject env per-request.
    * `handleVerify` MUST throw on non-OK so IDKit fires `onError` and skips
      `onSuccess`. Swallowing the error shows a false-positive success.
    * Signing key stays server-side only. Never place it in src/config/*, any
      *.functions.ts module, or any file the router entry imports.
  VERIFIED-WORKING SHAPE (target this):
    preset=orbLegacy, protocol_version="3.0", responses[0].identifier="orb",
    upstream POST https://developer.world.org/api/v4/verify/${rp_id},
    upstreamStatus=200, upstreamContentType="application/json",
    verifyIdSource="body.rp_id", fallbackTried=false.
  FAILURE -> DIAGNOSIS:
    * `onError { errorCode: "failed_by_host_app" }` and `/verify` never called
      -> using `deviceLegacy` on an Orb-backed World ID 4.0 app. Switch preset
      to `orbLegacy({ signal })`.
    * Upstream returns 403 with `text/html` body -> World edge blocked the
      request shape. Add `Accept: application/json` + `User-Agent`, then fall
      back to `app_id` on non-JSON 403.
    * Proof succeeds once but UI re-mounts to "Connect World App" -> failure
      branch cleared the proof-received state. Keep it; only reset on explicit
      "Re-verify".
    * Signal-mismatch verify failure right after wallet auth -> `signal`
      mutated between widget open and handleVerify. Freeze it in a ref.
    * "Open World App" silent on prod -> using v2 widget with production
      app_id. Switch to v4 `IDKitRequestWidget` as above.
    * `rp-signature 500 no_rp_config` -> `WORLDID_RP_ID` or
      `WORLDID_RP_SIGNING_KEY` missing. Add via Lovable secrets, redeploy.
    * `verify 400 invalid_proof` right after World App shows "successful" ->
      server is forwarding `{ rp_id, idkitResponse }` wrapper instead of the
      unwrapped `idkitResponse` proof. Fix per rule above.
    * `verify 400 action_not_found` -> action string doesn't match the one
      created in the portal. Match verbatim (case-sensitive).
    * Build crashes with `window is not defined` -> IDKit imported outside
      `<ClientOnly>`. Move the import into the lazy `worldid-inner.tsx`.
    * User reports "same issue" but debug shows upstreamStatus 200 JSON with
      fallbackTried=false -> it IS working; the UI never displayed a clear
      success state. Add an explicit "Verified human" line and collapse debug.
- DASHBOARD PREREQUISITE (one-time): Privy dashboard -> Gas sponsorship
  -> App pays -> add "World Chain Sepolia" -> toggle "Allow transactions
  from the client" ON. Without this, sendTransaction hangs silently.
- Hardhat in /contracts (kept outside the Vite bundle). Install
  `@nomicfoundation/hardhat-toolbox` AND `@nomicfoundation/hardhat-verify@latest`
  (>=3.x — older versions still hit Worldscan v1 and fail with
  "You are using a deprecated V1 endpoint, switch to Worldscan V2 (chainid=4801)").
- hardhat.config.cjs MUST use the Worldscan v2 (chainid=4801) single-key shape (NOT the per-network map):
    require("@nomicfoundation/hardhat-toolbox");
    require("@nomicfoundation/hardhat-verify");
    module.exports = {
      solidity: { version: "0.8.24", settings: { optimizer: { enabled: true, runs: 200 } } },
      networks: { worldchainSepolia: {
        url: process.env.SEPOLIA_RPC_URL || "https://worldchain-sepolia.g.alchemy.com/public",
        accounts: [process.env.METAMASK_PRIVATE_KEY.startsWith("0x")
          ? process.env.METAMASK_PRIVATE_KEY : "0x" + process.env.METAMASK_PRIVATE_KEY],
        chainId: 4801,
      } },
      etherscan: { apiKey: process.env.ETHERSCAN_API_KEY },   // single string, NOT { sepolia: ... }
      sourcify: { enabled: false },                            // silences the v2.x prompt
    };
- Deploy: `npx hardhat run scripts/deploy.cjs --network worldchainSepolia`.
- Verify (run RIGHT AFTER deploy, no constructor args for these contracts):
  `npx hardhat verify --network worldchainSepolia <address>`
  On success Worldscan returns "Successfully verified contract … on the block explorer"
  and the source becomes readable at
  `https://sepolia.worldscan.org/address/<address>#code`.
- Write the deployed address to `src/data/contract.json` so the UI links to
  `https://sepolia.worldscan.org/address/<address>`.

CONTRACT (contracts/DesignerDirect.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title SocialLogDesignerDirect
/// @notice Connect designers directly with suppliers using onchain verified transactions and profiles.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract SocialLogDesignerDirect {
    event Logged(address indexed author, string cid, uint256 at);
    /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
    function log(string calldata cid) external {
        emit Logged(msg.sender, cid, block.timestamp);
    }
}
```

USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded wallet auto-provisioned.
2. Every supply chain transparency action the user performs is sent as a sponsored World Chain Sepolia tx (`log(payload)`) and displayed with an Worldscan link. No wallet popups.
3. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"

REQUIRED SECRETS (Lovable -> Project Settings -> Secrets):
- METAMASK_PRIVATE_KEY  World Chain Sepolia deployer key. Fund it: https://www.alchemy.com/faucets/world-chain-sepolia
- SEPOLIA_RPC_URL       Alchemy World Chain Sepolia HTTPS endpoint (https://eth-sepolia.g.alchemy.com/v2/<key>). Create a free app at https://dashboard.alchemy.com/ -> copy the HTTPS URL. Public RPCs throttle/fail under hackathon load — Alchemy is required.
- ETHERSCAN_API_KEY     For `npx hardhat verify`. Get: https://etherscan.io/myapikey
- PRIVY_APP_ID          Google sign-in + sponsored tx. Docs: https://docs.privy.io/llms-full.txt
- PINATA_JWT            IPFS uploads (only if app pins media). Docs: https://docs.pinata.cloud/llms-full.txt

CREDIT (must appear in UI footer AND as NatSpec on every deployed contract):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Appendix · Market

Market sizing.

TAM
$1.2B
fashion design software
SAM
$130M
supply chain platforms
SOM
$13M
blockchain transparency solutions

Indicative figures for hackathon pitches — refine with your own research before raising.

See also

Adjacent entries.