PACT (Purchase Agreement for Community Tokens) is a prototype for raising small rounds by creating a cap table and selling a slice of the project's tokens (ERC-1155) along a bonding curve. A minimum threshold makes the raise refundable if it is not met by the close date.
The app currently targets Base mainnet and uses USDC for purchases.
Status: prototype. The contracts are unaudited and the lifecycle flows have only been exercised with small amounts. Use at your own risk and with caution.
/— connected-wallet dashboard, or a short explainer for what PACT is and how it works./create— issuer form for creating a PACT and deploying the onchain offering./pacts/:id— issuer dashboard for allocations, offering state, lifecycle actions, and cap table./pacts/:id/allocations/:allocationId— buyer-facing purchase and receipt page.
Under the hood:
src/pages/+src/lib/— the ES modules behind each page (built with Vite; styling is Tailwind v4 compiled at build time).server.js+server/— single Node process serving the Vite build (dist/) plus/api.contracts/— theOfferingandOfferingFactorySolidity sources.data/pact.sqlite— local runtime database, ignored by git.
More detail:
- Architecture (includes a full repository layout map)
- Onchain Offering
- Deployment
- Testing
Use Node 22.15.1 or newer. With asdf:
asdf install
asdf local nodejs 22.15.1Install dependencies:
npm installStart the dev server (Vite with the API mounted in-process):
npm run devOpen the URL Vite prints (http://localhost:5173/ by default).
To run the production shape locally, build and start the Node server:
npm run build
npm startOpen http://localhost:7228/.
By default the SQLite database is stored at data/pact.sqlite. Override it with:
PACT_DB_PATH=/tmp/pact.sqlite npm startReset a local database on boot with:
PACT_RESET_DB=1 npm startLocal defaults are enough to run the app. Production should set:
PACT_DB_PATH=/data/pact.sqlite
SPLITS_EXPLORER_API_KEY=...Optional:
PORT=7228
SPLITS_EXPLORER_GRAPHQL_URL=https://api.splits.org/graphql
PACT_RESET_DB=1Do not use PACT_RESET_DB=1 in production unless intentionally clearing data.
The browser code reads contract ABIs and the deployed OfferingFactory address
from src/generated/offering-contracts.js. All contract reads go through the
public Base RPC; the connected wallet is only asked to switch chains and sign.
Current Base OfferingFactory (unaudited — see status note above):
0x8bE9950470e0faC28Ed0fa590D972b466a6E0FE3
Official Base Liquid Split factory:
0xdEcd8B99b7F763e16141450DAa5EA414B7994831
Regenerate the contract exports (requires Foundry) with:
npm run build:contractsThe generated file is checked in so frontend builds do not require Foundry.
Run Solidity tests:
forge testRun API/domain tests:
npm testRun browser flow tests:
npm run test:e2enpm run test:e2e builds the app with Vite before running Playwright.
See Deployment. The short version:
- Fly app
splits-pactruns from the checked-infly.toml. - Persistent volume
pact_datais mounted at/data. SPLITS_EXPLORER_API_KEYis set as a Fly secret.- GitHub secret
FLY_API_TOKENenables auto-deploys frommain. - Manual deploys still work with
fly deploy -a splits-pact.
- Base mainnet only.
- SQLite is the persistence layer; Fly deployment needs a persistent volume.
- Allocation links are unauthenticated and rely on unguessable IDs.
- Issuer/buyer authorization is wallet-address gated in the UI, not
signature-authenticated, and the cached onchain snapshots (
offering-state,cap-table-state) are client-reported. Both are display conveniences; the contract remains the source of truth. - Lifecycle flows have been manually tested with dust, but still need broader real-world testing before public use.