Settle up, on-chain.
Ponti is a non-custodial primitive for shared expenses between two people. They deploy a group contract together, record expenses against it, and settle the running balance in USDC directly between their wallets, on-chain, with no third party holding funds in between.
It's a full-stack monorepo: Solidity contracts (the source of truth), a React app that makes the contract usable without crypto knowledge (email login, an embedded smart account, gas sponsored via ERC-4337), and a subgraph that serves the reads.
Proof of concept: complete and frozen. Ponti was built to demonstrate non-custodial, on-chain expense settlement end to end. M1 (the contract) and M2 (the gasless onboarding app) are both complete on Base Sepolia testnet. The project is not under active development; the later milestones below were scoped during design and deliberately not pursued.
M1 is deployed and verified on Base Sepolia, PontiFactory at 0x1746…Cb210. M2 works end to end: create a group, add/edit/delete expenses, and settle, all gasless. Live demo, running on Base Sepolia testnet.
The live demo runs on Base Sepolia testnet. Sign in with an email — Ponti provisions an embedded smart account for you and sponsors gas, so you never need a wallet, a seed phrase, or ETH.
There are two ways to exercise it, depending on how far you want to go:
- Quick look (one account). Add someone by pasting any Ethereum address as their "Ponti ID" — it doesn't have to belong to a real Ponti user. You can create the group, add/edit/delete expenses, and watch the net balance move. This exercises everything except settlement.
- Full loop (two accounts). Sign in with two different emails (two browsers, or a private window), add each other, and post an expense. Then settle: grab testnet USDC from a faucet (the in-app Add funds button links to one) and settle from the side that owes. Settlement is debtor-only by design and moves USDC directly between the two wallets.
- Two people deploy a Ponti group together: one smart contract, one address, just for them.
- Either person can record a shared expense at any time. The contract updates a single net balance. No money moves yet.
- Expenses can be edited or deleted. The contract recomputes the balance accordingly. A full audit trail is preserved on-chain.
- When the debtor wants to settle up, they call
settle(). A single transaction approves exactly the amount owed and moves it from the debtor's wallet to the creditor's wallet, then resets the balance to zero, with no standing allowance and no funds ever held by the contract.
graph TD
User -->|email / social login| Privy
Privy -->|embedded Kernel smart account| App
App -->|encodes UserOp| Pimlico
Pimlico -->|sponsors + bundles| EntryPoint
EntryPoint --> KernelAccount[Kernel smart account]
KernelAccount -->|executeBatch| Contracts["PontiFactory / PontiGroup<br/>Base Sepolia"]
App -->|GraphQL: groups, expenses, settlements| Subgraph[Subgraph]
App -->|readContract: balance, USDC| RPC["public RPC<br/>Base Sepolia"]
Contracts -->|events| Subgraph
The contract is the source of truth; everything off-chain is plumbing on top of a contract that works standalone.
- Writes are sponsored UserOperations (ERC-4337): email login creates an embedded Kernel smart account, the first write deploys it, and Pimlico sponsors gas, so the user never holds ETH or a seed phrase.
- Settlement batches a USDC
approvefor the exact debt and thesettle()call into oneexecuteBatch. Funds move debtor to creditor viasafeTransferFrom, and the contract never holds a balance. - Reads split between a subgraph (lists and history; dynamic data-source templates handle the per-group contracts deployed at runtime) and direct
readContractcalls (balances). After a write, the app waits for the subgraph to index the new block before refreshing, and degrades honestly if the indexer lags. - Contracts are tested with Foundry (unit, fuzz, invariant, and fork tests) and verified on Basescan.
For the full picture (component map, data flows, and the decision log) see docs/architecture.md.
Ponti is a proof of concept; its scope was deliberately bounded.
Product
- Expenses split 50/50 only. Custom ratios would require a contract change.
- Two members per group. Multi-party groups were scoped but not built.
- USDC only. No other tokens or fiat.
- Names are stored per-device, in the browser, and not synced. This is a deliberate choice to avoid running a backend; the same address can show a different name on another device.
- You add someone by pasting their address. There are no QR codes or invite links.
- No fiat on-ramp or off-ramp. "Add funds" links to a testnet faucet, and there is no cash-out path (real on/off-ramps are mainnet-only).
Technical
- Testnet only (Base Sepolia), and not audited. Do not use with real funds.
- Reads come from a free-tier hosted subgraph that can lag or stall. When it does, the app says so plainly and balances stay correct, since they are read straight from the contract.
| Directory | Contents |
|---|---|
contracts/ |
Solidity contracts, Foundry test suite, deployment scripts |
app/ |
Vite + React SPA, the M2 onboarding layer |
subgraph/ |
Subgraph (AssemblyScript mappings, schema, deploy config) |
docs/ |
Design rationale, architecture, and per-component specs |
Ponti is built with Foundry.
git clone https://github.com/aliersh/ponti.git
cd ponti/contracts
forge install # fetches the forge-std and openzeppelin-contracts submodules
forge build
forge testMost of the suite (unit, fuzz, and invariant tests) runs with no configuration. The fork tests run against Base Sepolia and read the BASE_SEPOLIA_RPC_URL environment variable; set it in a contracts/.env file to run them.
cd ponti/app
pnpm install
pnpm gen:abi # generates app/src/generated/contracts.ts from Foundry build artifacts
pnpm devRequired environment variables (copy app/.env.example to app/.env):
| Variable | Where to get it |
|---|---|
VITE_PRIVY_APP_ID |
Privy dashboard, public, safe in the client bundle |
VITE_SUBGRAPH_URL |
the Ponti subgraph's GraphQL query endpoint |
VITE_PIMLICO_SPONSORSHIP_POLICY_ID |
Pimlico dashboard, optional; leave empty if not required by your policy |
VITE_RPC_URL |
optional, overrides the default Base Sepolia public RPC for readContract calls |
Built with graph-cli (pnpm build from subgraph/) and deployed to Goldsky. See docs/subgraph-spec.md for the entity schema and query design.
docs/architecture.md: the system architecture, component map, data flows, and decision logdocs/design.md: the design and the reasoning behind itdocs/contract-spec.md: the contract's function-by-function specificationdocs/app-spec.md: the web app specification (flows and integration)docs/subgraph-spec.md: the indexer (subgraph) specification
Ponti is deployed to testnet (Base Sepolia) only and has not been audited. Do not use it with real funds. The contract is non-custodial by design: it never holds funds, and settlement moves USDC directly between members' wallets. That property has not been independently reviewed.
A proof of concept, not a maintained product. M1 and M2 were completed; the later milestones were scoped during design but deliberately not pursued.
| Milestone | Theme | Outcome |
|---|---|---|
| M1 | Two-party non-custodial IOU contract | Complete (deployed and verified, Base Sepolia) |
| M2 | Embedded smart-account auth, gasless UX (Base Sepolia) | Complete (functional end to end) |
| M3 | Multi-party groups and debt-graph simplification | Scoped, not pursued |
| M4 | Off-chain integration: bank-feed ingestion | Scoped, not pursued |
See docs/design.md for the reasoning behind the milestone ordering.
Built by Ariel Diaz.
MIT.
