A crypto-economic pull request gating system for open-source repositories.
Open-source contribution lacks accountability. Contributors can open multiple pull requests without commitment, and maintainers have no lightweight way to regulate quality without significant manual effort. The result: PR queues clogged with abandoned work, frustrated maintainers, and reduced trust in the contribution pipeline.
PRStake adds a minimal staking layer to the GitHub contribution workflow. Contributors stake once to unlock PR slots. Responsible behavior is rewarded with a full deposit return. Irresponsible behavior — leaving PRs open, spamming — is naturally rate-limited through liquidity lock rather than punitive slashing.
The key insight: staking acts as both commitment signal and rate-limiter simultaneously.
| Resource | URL |
|---|---|
| Architecture | architecture.md |
| Demo | youtube.com/watch?v=XkVcxlGLISo |
| Contract Explorer | Rootstock Testnet |
1. Log in with GitHub
2. Connect wallet & sign binding message
3. Deposit tRBTC once
4. Deposit unlocks up to 10 concurrent PR slots
5. Each open PR consumes one slot
6. Closing or merging a PR frees one slot
7. When all PRs are closed, claim your full deposit back
No slashing. No penalties. Only time-locking as a natural deterrent.
flowchart LR
U[Contributor]
GH[GitHub OAuth]
MM[MetaMask Wallet]
UI[PRStake Dashboard - Next.js on Vercel]
subgraph APP[Application Layer]
AUTH[NextAuth Session]
BINDAPI[API /bind]
CHECKAPI[API /check/:github_username]
end
subgraph DATA[Data Layer]
SB[(Supabase — bindings table)]
end
subgraph CHAIN[Blockchain Layer]
RS[Rootstock Testnet]
VAULT[PRStakeVault Contract]
end
subgraph CI[GitHub Automation]
PR[Pull Request Open / Close]
ACT[GitHub Actions workflow]
end
U --> UI
UI --> GH
UI --> AUTH
U --> MM
MM --> UI
UI --> BINDAPI
BINDAPI --> AUTH
BINDAPI --> SB
UI --> VAULT
UI --> RS
RS --> VAULT
PR --> ACT
ACT --> CHECKAPI
CHECKAPI --> SB
CHECKAPI --> VAULT
ACT --> VAULT
ACT --> PR
SB -.-> AUTH
SB -.-> VAULT
sequenceDiagram
autonumber
actor C as Contributor
participant D as PRStake Dashboard (Vercel)
participant GH as GitHub OAuth / NextAuth
participant MM as MetaMask
participant BA as /api/bind
participant SU as Supabase (bindings)
participant VA as PRStakeVault (Rootstock)
participant GA as GitHub Actions (pr-gate)
participant CA as /api/check/:github_username
C->>D: Open app and click Sign in
D->>GH: Start GitHub OAuth
GH-->>D: Session with github_username
C->>D: Connect wallet
D->>MM: Request accounts on chain 31
MM-->>D: wallet_address
C->>D: Click Bind Wallet
D->>MM: Sign message "I am github:USERNAME"
MM-->>D: signature
D->>BA: POST github_username, wallet_address, signature
BA->>BA: verifyMessage(signature)
BA->>SU: Upsert github_username ↔ wallet_address
SU-->>BA: Saved
BA-->>D: Bind success
C->>D: Deposit tRBTC
D->>VA: deposit(value) tx via wallet signer
VA-->>D: Tx confirmed
C->>GA: Open Pull Request
GA->>CA: GET eligibility by github_username
CA->>SU: Resolve bound wallet
SU-->>CA: wallet_address
CA->>VA: Read deposit and openPRCount
VA-->>CA: on-chain state
CA-->>GA: eligible true/false
alt Eligible
GA->>VA: onPROpen(wallet)
VA-->>GA: Updated openPRCount
GA-->>C: Comment: Slot used (X/10)
else Ineligible
GA-->>C: Comment: Not eligible — PR closed
end
C->>GA: Close Pull Request
GA->>VA: onPRClose(wallet)
VA-->>GA: Decremented openPRCount
Main contract: contracts/PRStakeVault.sol
Network: Rootstock Testnet
| Method | Access | Description |
|---|---|---|
deposit() |
Public | Deposit tRBTC to activate PR slots |
onPROpen(address wallet) |
Trusted action | Increment open PR count for wallet |
onPRClose(address wallet) |
Trusted action | Decrement open PR count for wallet |
claimDeposit() |
Public | Reclaim deposit when all PRs are closed |
- Maximum 10 open PRs per wallet at any time
- Only the designated
TRUSTED_ACTION_ADDRESSmay callonPROpen/onPRClose claimDeposit()is only callable whenopenPRCount == 0
Workflow: .github/workflows/pr-gate.yml
- On PR open: calls
/api/check/:github_username— closes the PR automatically if contributor is ineligible - On PR close: decrements the contributor's open slot count on-chain
No maintainer intervention required.
| Layer | Technology |
|---|---|
| Smart Contract | Solidity + Hardhat (Rootstock Testnet) |
| Frontend | Next.js 14 (App Router) |
| Auth | NextAuth.js (GitHub OAuth) |
| Database | Supabase |
| CI/CD | GitHub Actions |
- Node.js 18+
- A Rootstock-compatible wallet (MetaMask with chain 31)
# Install dependencies
npm install
# Compile and deploy contracts locally
npm run hardhat:compile
npm run hardhat:deploy
# Start the development server
npm run devOpen http://localhost:3000.
This project requires a public.bindings table in Supabase.
npx supabase login
npx supabase link --project-ref <your-project-ref>
npx supabase db pushMigration file: supabase/migrations/202604010001_create_bindings.sql
create table if not exists public.bindings (
wallet_address text primary key,
github_username text unique not null,
signature text not null,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create index if not exists idx_bindings_github_username
on public.bindings (github_username);Copy .env.example to .env.local and fill in the values below.
| Variable | Description |
|---|---|
SUPABASE_URL |
Supabase project URL |
SUPABASE_SERVICE_ROLE_KEY |
Supabase service role key (server-side only) |
RPC_URL |
Rootstock RPC endpoint |
VAULT_ADDRESS |
Deployed PRStakeVault contract address |
GITHUB_CLIENT_ID |
GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET |
GitHub OAuth app client secret |
NEXTAUTH_SECRET |
Random secret for NextAuth.js session encryption |
NEXTAUTH_URL |
Canonical URL of your deployment |
NEXT_PUBLIC_VAULT_ADDRESS |
Vault address exposed to the browser |
NEXT_PUBLIC_RPC_URL |
RPC URL exposed to the browser |
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID |
WalletConnect project ID |
| Variable | Description |
|---|---|
PRIVATE_KEY |
Wallet private key for contract deployment |
TRUSTED_ACTION_ADDRESS |
Address authorised to call PR open/close hooks (optional) |
DASHBOARD_URL |
Public URL of the dashboard, used in PR comments |
PRStake deliberately avoids slashing. Funds are never taken from contributors — they are only time-locked until outstanding PRs are resolved. This makes the system:
- Beginner-friendly — contributors are not penalised for inexperience
- Fair — bad actors are slowed down, not financially punished
- Self-regulating — economic friction replaces manual moderation
- Extensible — the staking primitive naturally supports future features such as on-chain reputation scores, contributor rewards, and DAO-based governance
PRStake is intentionally minimal at its core — but the staking primitive opens the door to a richer ecosystem:
| Area | Description |
|---|---|
| On-chain Reputation | Accumulate a non-transferable score based on merge rate, PR quality, and slot discipline — visible to any repo or DAO |
| Tiered Slots | Higher deposits unlock more concurrent PR slots, allowing prolific contributors to scale without friction |
| Contributor Rewards | Maintainers or protocols can top up a reward pool; merged PRs trigger proportional payouts from the vault |
| DAO Governance | Governance token holders vote on stake thresholds, slot limits, and trusted action addresses per repository |
| Cross-repo Identity | A single staked identity works across multiple repositories without re-depositing |
| Slash Conditions (opt-in) | For high-security or bounty repos, maintainers can enable configurable slashing on closed-without-merge PRs |
| Reputation-gated Reviews | Extend the gating model to code reviews — require reviewers to stake before their approvals are counted |
| Analytics Dashboard | Public contributor health metrics: slot utilisation, merge rate, average PR lifetime, on-chain activity |
The current architecture — Supabase bindings, a single vault contract, and a GitHub Actions hook — is designed to support all of the above without a rewrite.
PRStake is itself an open-source project. Contributions are welcome — and yes, they are gated by PRStake.
MIT