Pay a handle, not an address.
Non-custodial social payments: send stablecoins, Robinhood Stock Tokens and xStocks to anyone's GitHub, X, Telegram,
Discord or Farcaster handle, across Robinhood Chain, Solana and Arc. Your wallet signs every transfer.
Vision · Features · How it works · Architecture · Getting started · Docs
Stack: TypeScript · React · Vite · Express · PostgreSQL · Solidity (Foundry) · Rust (Solana program) · Node.js 24
Money should move like a message. Everyone already has a name online: a GitHub handle, an X account, a Telegram username. What they rarely have is a wallet address you can type from memory. Crypto still asks people to copy and paste 42 characters, check the first four and the last four, and hope nobody swapped the middle.
HaPaPay turns a verified social handle into a payment address that works on more than one chain. You write what you want in plain words, "send 25 USDG to @toly on X", review exactly what will happen, and your own wallet signs it.
A few rules hold everywhere:
- Identity is the address. Handles are linked to wallets only through each platform's official sign-in; a typed name is never proof of anything.
- Your keys, your signature. The server prepares and verifies transactions; it never holds your keys, never signs a transfer and never broadcasts one.
- Anyone can be paid. Someone who has not joined yet can still receive money. It waits in an on-chain vault until they prove the handle is theirs, or it goes back to you.
- Verify, don't trust. Every prepared transaction is checked in the browser against what you reviewed, and nothing is recorded until its receipt is read back from the chain.
HaPaPay is not tied to one network. Each request runs where its asset lives, and where you hold it.
| Send to verified accounts on GitHub, X, Telegram, Discord and Farcaster. One person or up to ten in one request, with an optional note written on chain. | USDC and USDG, 195 Robinhood Stock Tokens on Robinhood Chain and 951 xStocks on Solana, each pinned by address and read back from chain before it is listed. |
| A deterministic intent engine reads amounts, assets, handles, platforms and notes, in English and in Turkish. It asks when something is missing instead of guessing; an optional LLM reader is treated as untrusted input. | Pay people who have not joined. Funds wait in an escrow contract or a Solana program and are claimed with a one-time attestation once the recipient verifies the account. Unclaimed links are refundable. |
| The asset picks the network, and the sender's own holdings break ties: a request goes where the wallet can actually pay it, and the review says why. | Email, social login or an existing wallet through Privy gives one account with an EVM and a Solana address. Without Privy, injected EVM wallets and Wallet Standard Solana wallets work. |
| A 1% fee, added on top and shown before signing; the recipient always gets exactly the amount written. The 1% rate and its split are constants in the contracts the operator registers; an owner can only lower it for holders of a token it names. | Social points (SP) for payments and claims verified on chain and for linking accounts, invite rewards, and an admin panel where every change, SP adjustments included, is signed by an admin wallet and logged first. |
A local install configured for mainnet; with the .env.example defaults, USDC runs on Arc Testnet
and Stock Tokens and Solana show as locked. Vault links open once the operator registers a vault on a network.
A payment is a conversation that ends in your wallet. The server reads and checks; your wallet signs; the chain is the record.
%%{init: {"sequence": {"wrap": true, "mirrorActors": false}}}%%
sequenceDiagram
autonumber
actor S as Sender
participant D as Desk (browser)
participant A as HaPaPay API
participant I as Identity store
participant C as Chain
S->>D: "send 25 USDG to @toly on X"
D->>A: POST /api/chat
A->>A: Read the intent (amount, asset, handle, platform, note)
A->>I: Resolve @toly on X to a verified wallet
A-->>D: Review: network, recipient, amount, 1% fee, total
S->>D: Confirm
D->>A: Prepare the transaction
A->>I: Resolve the recipient again (409 if it changed)
A->>C: Check balances and allowances (and simulate on Robinhood Chain and Solana)
A-->>D: Unsigned transaction
D->>D: Rebuild it from the review and compare byte for byte
D->>S: Wallet asks for a signature
S->>C: Signed transaction
D->>A: Confirm with the transaction hash
A->>C: Read the receipt and match the exact transfer logs
A-->>D: Recorded in activity
When a handle has no verified wallet yet, the money can wait in a vault. A GitHub or Farcaster link waits for the account's immutable ID, and so does an X link while the server can look X accounts up. Discord and Telegram, which cannot be looked up by name, wait for the name (as X does without lookups) and accept only an account whose name was confirmed after the link was made and, where the platform's IDs carry a creation time, an account older than the link.
%%{init: {"sequence": {"wrap": true, "mirrorActors": false}}}%%
sequenceDiagram
autonumber
actor P as Payer
participant V as Vault (escrow or program)
participant A as HaPaPay API
actor R as Recipient
P->>V: Fund the link: amount + 1% fee, open for 3 to 30 days
A->>A: Record it from the exact PaymentCreated event
P-->>R: Shares the claim link
R->>A: Signs in with the platform's official login
A->>A: Check the verified account matches the link's lock
A-->>R: One-time attestation (expires within 10 minutes)
R->>V: Claim: amount to the recipient, fee to the protocol
Note over P,V: If nobody claims in time, only the payer can refund, fee included
flowchart TD
Q["A request that names no network"] --> X{"An xStock written with its x (TSLAx),<br/>or a ticker Robinhood Chain does not list?"}
X -->|yes| SOLX["Solana"]
X -->|no| G{"Solana switched on, the sender (when signed in)<br/>has a Solana address, and every recipient has one,<br/>or the only recipient can wait in the Solana vault?"}
G -->|no| HOME["The asset's home network:<br/>Robinhood Chain for tickers and USDG,<br/>Arc for USDC"]
G -->|yes| A{"Which asset?"}
A -->|"Stock Token ticker or USDG"| RH{"Robinhood Chain can pay everyone now,<br/>and the wallet does not hold enough<br/>only on Solana?"}
RH -->|yes| RHC["Robinhood Chain"]
RH -->|no| SOL["Solana"]
A -->|USDC| U{"Arc can pay everyone now, Solana holds<br/>less than the payment and its fee,<br/>and Arc holds enough?"}
U -->|no| SOL
U -->|yes| ARC["Arc"]
Without a network written, a ticker only Robinhood Chain lists (such as COST) goes on Robinhood Chain. To choose a network for an asset it carries, write it in the request itself and put any note or reason last ("Send 5 USDC to @bob on X on Arc for rent", "Arc üzerinden @bob'a x'te 5 USDC gönder, kira için"). Each draft is built for a network that carries its asset, and every review names the network it uses and, when holdings decided it, how to ask for the other one; check it before signing.
| Network | Assets | How a payment settles | Vault |
|---|---|---|---|
| Robinhood Chain (4663) | 195 Robinhood Stock Tokens (178 stocks, 17 ETFs), USDG | approve + HaPaPayRouter.pay: amount to the recipient, fee split in the same transaction |
StockClaimEscrow |
| Solana (mainnet) | USDC, USDG, 951 xStocks | SPL Token and Token-2022 transfers plus the fee to the treasury in one transaction; batches split to fit | hapapay-vault program |
| Arc (Mainnet 5042, Testnet 5042002) | USDC | approve + HaPaPayRouter.pay, fee through HaPaPayFeeForwarder |
StockClaimEscrow |
The fee contracts become part of a network when the operator registers its escrow: until then Robinhood Chain sends plain transfers without a fee, and Arc Mainnet waits for them.
| Platform | Verified through |
|---|---|
| GitHub | OAuth |
| X | OAuth 2.0 with PKCE (S256) |
| Telegram | Log in with Telegram, HMAC-verified |
| Discord | OAuth (identify) |
| Farcaster | Sign in with Farcaster (nonce, domain, signature and FID checks) |
flowchart LR
subgraph Browser
Desk["Desk SPA<br/>React · Vite"]
Wallets["Privy · injected EVM wallets<br/>Wallet Standard (Solana)"]
end
subgraph Server["HaPaPay API · Express"]
Chat["Intent engine"]
Identity["Identity and OAuth"]
Pay["Payment preparation<br/>and receipt checks"]
Vaults["Vault services and<br/>claim attestor"]
Points["SP, invites, admin"]
end
DB[("PostgreSQL")]
LLM["Optional LLM reader<br/>(untrusted)"]
Providers["GitHub · X · Telegram<br/>Discord · Farcaster"]
subgraph Chains
RHC["Robinhood Chain"]
SOL["Solana"]
ARC["Arc"]
end
Desk <--> Server
Desk --> Wallets
Wallets -- "signed transactions" --> Chains
Chat -.-> LLM
Identity <--> Providers
DB <--> Server
Pay -- "reads, simulations, receipts" --> Chains
Vaults -- "reads, attestations" --> Chains
The browser never trusts the server blindly and the server never trusts the browser: prepared transactions are rebuilt and compared in the browser before a wallet opens, and records are written only from receipts the server reads itself. More in docs/architecture.md and docs/protocol.md.
.
├── src/ Desk (React SPA) and the domain rules shared with the server
│ ├── components/ Home, desk, docs, operator and admin pages; dot-matrix UI kit
│ ├── domain/ Pure logic: intent parsing, assets, fees, vault locks,
│ │ contract artifacts
│ └── wallet/ Privy and injected-wallet bridges
├── server/ Express API: chat, identity, payments, vaults, receipts,
│ SP and the admin panel
├── contracts/ Solidity: StockClaimEscrow, HaPaPayRouter, HaPaPayBurnVault,
│ HaPaPayFeeForwarder, ArcIdentityRegistry
├── foundry-test/ Forge unit, fuzz and invariant tests for the contracts
├── programs/hapapay-vault/ Solana vault program (Rust, native solana-program)
├── scripts/ Allowlist sync, artifact generation, migrations, key setup
├── tests/ Node test suite: API, services, lifecycles, UI rules
├── public/ Static files and the pinned Solana program build
├── docs/ Architecture, protocol, security, self-hosting, decisions
├── server.ts Serverless entry (Vercel)
└── Dockerfile Container build
The compiled Solana program, public/solana/hapapay_vault.so, is committed on purpose: it is the exact build whose size
and SHA-256 the server and the desk check against the deployed program (see docs/protocol.md), so
it changes only through npm run build:solana-vault with the pinned toolchain.
- No custody. The server prepares exact transactions, checks balances and allowances (and simulates on Robinhood Chain and Solana), and verifies receipts. The only thing it signs for the chain is a short-lived claim attestation for a vault link.
- The browser rebuilds every prepared call from the reviewed draft (network, token, sender, recipient, amount, fee, note) and refuses to open the wallet on any difference.
- Accounts are linked through official OAuth, signatures or HMAC; one provider identity belongs to one wallet at a time, and a recipient is resolved again right before preparation.
- Tokens are allowlisted by address and read back from chain; contracts and the Solana program are accepted only when their deployed code matches the committed artifacts.
- Tests: 600+ Node tests (including Anvil lifecycles and an isolated PostgreSQL run), 56 Forge tests with fuzzing and an invariant suite, and Slither static analysis.
- Not audited. The contracts and the program have not had an independent audit. Start with small amounts.
Read the full model in docs/security.md. To report a vulnerability, see SECURITY.md.
- Node.js 24 and npm
- PostgreSQL 16 for production (development runs on in-memory stores)
- Forge and Anvil come with
npm ci; nothing else is needed fornpm testandnpm run test:forge(Forge downloads solc 0.8.28 on its first run) - Optional: the Agave (Solana) tool suite for the program build and the validator tests, and pipx (with PyPI access)
for
npm run test:slither
git clone https://github.com/furkan3152/HaPaPay.git
cd HaPaPay
npm ci
cp .env.example .env # every value is optional in development
npm run dev # desk on http://localhost:5173, API on http://localhost:8787Without provider keys the desk still runs: networks and platforms that are not configured show as locked, with the reason the server reports.
| Command | What it does |
|---|---|
npm run dev |
Vite and the API together (restart for API changes) |
npm run check |
TypeScript project check |
npm test |
The Node test suite |
npm run test:forge |
Forge tests for the contracts (unit, fuzz, invariants) |
npm run test:slither |
Slither static analysis of the contracts |
npm run build |
Production build of the desk |
npm run migrate:database |
Apply the PostgreSQL migrations (explicit, never at request time) |
npm run sync:stock-tokens |
Rebuild the Stock Token allowlist from the registry and chain |
npm run sync:solana-stocks |
Rebuild the xStocks allowlist from the xStocks API and Solana |
npm run generate:stock-escrow |
Regenerate the contract artifacts from contracts/ |
npm run build:solana-vault |
Build the Solana program and pin its size and hash |
npm run readiness |
Check the settings and Arc's contracts on chain (see below) |
npm run readiness checks the session secret, database, sign-in apps, Farcaster RPC and OpenRouter and X keys, and
verifies the Arc network and contracts on chain; missing settings are printed by name only.
To include the real-PostgreSQL suites, point the tests at an empty, disposable database:
REAL_DATABASE_URL=postgres://postgres@127.0.0.1:5432/postgres HAPAPAY_ISOLATED_POSTGRES=1 \
npm testHaPaPay runs as a Vercel project (static desk on the CDN, the API as a function) or as a container (Dockerfile).
Production needs PostgreSQL, an HTTPS origin, a session secret of your own, an Optimism RPC for Sign in with
Farcaster, the OAuth apps you want, and the operator steps that deploy the contracts and the Solana program from
/operator/stock-escrow. The full checklist is in
docs/self-hosting.md.
| Document | Contents |
|---|---|
| Architecture | Components, request lifecycle, intent engine, routing, identity, data model |
| Protocol | Contracts, the Solana program, attestations, fees and their verification |
| Security model | Trust boundaries, threats and controls, known limitations |
| Self-hosting | Configuration, OAuth apps, database, operator setup, deployment |
| Invariants | Rules every change must keep |
| Design decisions | Why the system is built the way it is |
| Design system | The dot-matrix interface and its rules |
| Contributing | Workflow, tests and conventions |
The desk also serves its own user and operator guide at /docs.
A live HaPaPay instance runs on Arc Mainnet at siglos-pay.vercel.app. Its hostname and page title still carry the project's earlier name, Siglos Pay; the code and the product are HaPaPay.
HaPaPay is an independent project. It is not affiliated with, endorsed by, or officially connected with Robinhood Markets, Inc., the Solana Foundation, the xStocks issuer, Paxos, Circle, Arc, Privy or any social platform it supports.
Robinhood Stock Tokens and xStocks give economic exposure, not ownership of the underlying shares. They are not for U.S. persons, and other regional restrictions apply; the eligibility statements HaPaPay asks for are self-attestations, not a legal review. Nothing here is financial advice. The software is provided as is, without warranty, and its contracts are unaudited.
HaPaPay is released under the MIT License.
Built by Furkan Akdogan (@furkan3152).
