Search projects, hackathons, sponsors and tech

    ← All projects

    PayGate

    Team payments with built-in approval rules, settled instantly in stablecoins.

    Demo

    About

    PayGate is a treasury and spend-management tool for companies that pay  contractors, vendors and staff in stablecoins. Today most teams route every  payout through one person with wallet access — a bottleneck, a single point of  failure, and an approval trail that lives in email. PayGate replaces that  with a shared company treasury where every payment carries an enforced rule:  who can approve it, how many of them, and up to what amount.  A company signs up with an email and gets a Privy organization wallet as its  treasury — no seed phrase, no key for anyone to lose. Admins then define  approval groups, each backed by its own Privy key quorum and spending policy:  "Payroll — HR, one approval, up to 10,000 USDC", "Grants — the four-person  committee, two approvals, up to 50,000", "Vendors — finance, two approvals, no  ceiling". Groups attach to the treasury as scoped additional signers with  override_policy_ids, so Privy evaluates only the policy of whoever actually  signs. That's what lets one wallet enforce genuinely different limits per team  instead of needing a wallet per budget.  Any team member raises a payment — payee (an external address or a teammate by  name, resolved to their Privy embedded wallet), amount, asset and an attached  invoice. Before submitting they see exactly which rule will govern it and who  must approve. Payments under a group's threshold clear on the requester's own  signature; larger ones wait for a second approver. Approvals are signed in  each approver's browser with their own Privy key via  useAuthorizationSignature() — PayGate only relays the signature and never  holds signing material, so even a fully compromised backend cannot manufacture  an approval. Once enough signatures exist they're submitted together and  Privy verifies them against the treasury's key quorum before anything moves.  The treasury holds multiple assets — USDC, EURC, WETH and native ETH — with  per-asset balances and per-asset limits, handling both native value transfers  and ERC-20 calldata. Changing the rules is itself an approved action:  attaching a group is owner-level at Privy, so it requires the same sign-off as  a large payment. Everything lands in an audit trail recording who acted,  when, and the on-chain transaction, exportable to CSV with approvers included.  Settles in USDC on Base Sepolia. Live at usepaygate.xyz.

    How it's made

    Stack: Next.js 16 (App Router) with React 19 and TypeScript, Tailwind 4,  Prisma 7 against Postgres (Neon, provisioned through Vercel), and viem 2 for  chain reads and calldata encoding. Privy via @privy-io/react-auth 3 in the  browser and @privy-io/node 0.34 on the server. Deployed on Vercel at  usepaygate.xyz, settling on Base Sepolia.  How the pieces fit. Org setup runs a fixed sequence against Privy: create  users → create key quorums → create a spending policy → create the  organization → create the treasury wallet with entity: {type: 'organization'}  and the member quorum as an additional_signer carrying override_policy_ids.  Approval groups are the same shape — each one is a quorum plus a policy,  attached as another scoped signer, which is what lets a single wallet enforce  different limits depending on who signs. Payments store the exact transaction  at submit time so every approver signs identical bytes; signatures are  collected and submitted together, comma-delimited in the  privy-authorization-signature header. Prisma holds what Privy doesn't know  about: invoices, memos, roles, group membership and the audit trail.  The notable hacky parts:  @privy-io/node@0.34's public client is missing methods the docs show —  users.create, keyQuorums.create, and the entire organizations service exist  only on a private underlying client. I reach it through one narrow cast that  names exactly the surface used, typed with the SDK's own param types, so an  SDK change surfaces as a type error rather than at runtime.  I built the whole approval flow on Privy intents first, then had to tear it  out. intents.authorize() doesn't exist on either client — only the  IntentAuthorizeInput type ships — and the REST endpoint rejected signatures  built to the documented payload spec across eleven variants, including the  intent_id and timestamp fields that appear in the browser SDK's types but not  the server's. Separately, POST /v1/wallets/authenticate rejects valid Privy  access tokens on this app, so no server-side user signing key can be issued at  all. The way through was to sign in the browser with  useAuthorizationSignature() and have the server merely relay — which turned  out to be the better architecture, since PayGate then never touches signing  material.  additional_signers is replaced wholesale on a wallet update. Attaching a new  group by sending just that group would have silently detached every other one,  including the original member quorum and its capped policy — it would have  demoed perfectly and quietly broken every other rule. desiredSigners()  rebuilds the complete list every time.  Smaller things that mattered: invoice types are sniffed from magic bytes  rather than trusting Content-Type or the extension, and stored as Bytes in  Postgres because Vercel's filesystem is ephemeral. Money is integer base units  as BigInt everywhere with per-asset decimals — which caught a real bug where  the dashboard was adding micro-USDC to wei. Balances are cached server-side  for 10s after the public RPC started throttling under polling. Privy caps  display_name at 50 characters, which silently broke org creation for longer  names until I truncated.