A fully decentralized token ecosystem that runs entirely on GitHub. No servers, no wallets, no gas fees — just forks, pull requests, and consensus.
Live Balance Explorer: https://volta2030.github.io/gitcoin/ — Balances, Transactions, Validators tabs
| Blockchain concept | GitCoin equivalent |
|---|---|
| Full node | Fork of this repository |
| Transaction | Pull Request with UTXO file changes |
| Block | Merge commit on main |
| Hash chain | Git commit history |
| Validator / miner | Any GitHub account with a registered public key in validators/pubkeys.json |
| Consensus | ⌈2/3⌉ selected validators comment /approve |
| Double-spend guard | Git merge conflict (two PRs can't delete the same file) |
| Total supply | 4,294,967,295 GTC (fixed, no inflation) |
| Minimum unit | 1 GTC (integer amounts only) |
1. Pull the latest main branch
2. Run create_transaction.py — builds, signs, writes UTXO files, and git rm's inputs automatically
3. git add utxo/ && git commit && git push to a new branch, open a PR
4. validate-tx.yml verifies signature and UTXO ownership
5. On success: tx-valid label + Validator Vote Requested comment posted (same workflow, no separate trigger)
6. Selected validators comment /approve on the PR
7. When ⌈2/3⌉ approvals are reached, PR can be merged
8. update-pages.yml rebuilds the explorer (Balances / Transactions / Validators)
Non-TX PRs (key registration, code changes) skip validation entirely and get immediate
successstatus on both required checks, so they are never blocked.
pip install cryptographyYou also need:
- A GitHub account
- Git installed locally
ghCLI (optional, for convenience): https://cli.github.com
Click Fork at the top of this page. Your fork is your full node — it contains the entire ledger history.
git clone https://github.com/YOUR_USERNAME/gitcoin
cd gitcoin
python3 .github/scripts/generate_keypair.pyOutput:
⚠️ PRIVATE KEY — Keep this secret, never commit it:
<your-private-key-base64url>
✅ PUBLIC KEY — Share this in your REGISTER_KEY PR:
<your-public-key-base64url>
Store your private key in a password manager. If you lose it, you lose access to your coins. Never commit it to any repository.
Before you can send GTC, your public key must be in validators/pubkeys.json.
Create a PR to this repo's main branch with:
Changed file — add your entry to validators/pubkeys.json:
{
"existing_user": "their_key",
"YOUR_GITHUB_USERNAME": "YOUR_PUBLIC_KEY_BASE64URL"
}PR title: register: YOUR_GITHUB_USERNAME
PR body: anything (no TX_VERSION needed — the workflow detects this is not a TX and auto-approves both required status checks immediately).
Once a maintainer merges the PR, you can start transacting — and you are automatically added to the validator pool.
Visit the live balance explorer:
https://<owner>.github.io/gitcoin/
Or inspect the ledger directly:
git pull origin main
cat docs/ledger.jsonOr scan your UTXOs manually:
grep -rl '"owner": "YOUR_USERNAME"' utxo/git pull origin maingrep -rl '"owner": "YOUR_USERNAME"' utxo/Note the txid values (the filenames without .json).
python3 .github/scripts/create_transaction.pyYou will be prompted for:
- Your GitHub username
- Your private key
- Recipient username
- Amount to send (GTC)
- UTXO txids to spend (comma-separated)
- Optional memo
The script outputs the exact PR body to copy and the file changes to make. It can also write the output UTXO files automatically.
The script does git rm on input UTXOs and writes output files automatically. Just run:
git add utxo/
git commit -m "tx: YOUR_USERNAME → RECIPIENT 50 GTC"
git push origin <new-branch-name>Open a Pull Request to this repository's main branch.
- PR title:
tx: YOUR_USERNAME → RECIPIENT 50 GTC - PR body: paste the
TX_VERSION: 1 ...block from the script output
validate-tx.yml runs automatically. If valid:
tx-validlabel is attached- A Validator Vote Requested comment is posted listing selected validators and deadline
- Selected validators comment
/approve— anyone invalidators/pubkeys.jsonis eligible - When ⌈2/3⌉ approvals are reached, merge the PR
Anyone with a registered public key in validators/pubkeys.json is a validator.
- Generate your keypair:
python3 .github/scripts/generate_keypair.py - Open a PR adding
"YOUR_USERNAME": "YOUR_PUBLIC_KEY"tovalidators/pubkeys.json - Once merged, you are immediately in the validator pool
Scores in validators/registry.json influence selection probability. Anyone not listed defaults to 100 points.
| Action | Points |
|---|---|
Comment /approve on a valid TX |
+20 |
| Submitting a valid TX that gets merged | +5 |
Inactivity penalty: −10 points per week with no /approve activity.
When a transaction PR is validated:
- You receive a GitHub @mention in the Validator Vote Requested comment.
- Visit the PR, review the
utxo/file changes. - Comment exactly
/approveto cast your vote.
You have 48 hours. If the threshold is not reached, the PR is automatically expired.
TX_VERSION: 1
FROM: alice
TO: bob
AMOUNT: 50
INPUT_TXIDS: a1b2c3d4e5f6...,d4e5f6a1b2c3...
OUTPUT_TO_TXID: f7a8b9c0d1e2...
OUTPUT_CHANGE_TXID: e3d4c5b6a7f8...
MEMO: payment for work
SIGNATURE: <base64url Ed25519 signature>
| Field | Required | Description |
|---|---|---|
TX_VERSION |
✅ | Must be 1 |
FROM |
✅ | Must match PR author's GitHub login |
TO |
✅ | Recipient GitHub username |
AMOUNT |
✅ | Integer GTC to send (minimum: 1 GTC) |
INPUT_TXIDS |
✅ | Comma-separated txids of UTXOs you are spending |
OUTPUT_TO_TXID |
✅ | txid of new UTXO file added for the recipient |
OUTPUT_CHANGE_TXID |
if change | txid of change UTXO returned to you |
MEMO |
optional | Free-text note |
SIGNATURE |
✅ | Ed25519 signature over the canonical message |
Conservation rule: sum(inputs) == AMOUNT + change. No GTC can be created or destroyed.
Note: All amounts are integers. The minimum transactable unit is 1 GTC. Decimal amounts are not supported.
TX_VERSION: REGISTER_KEY
USERNAME: alice
PUBLIC_KEY: <base64url Ed25519 public key>
PR must only modify validators/pubkeys.json by adding one new entry.
Each file in utxo/ represents one unspent coin:
{
"txid": "a1b2c3d4...",
"owner": "alice",
"amount": 100,
"unit": "GTC",
"created_at_block": "<merge commit SHA>",
"created_at_height": 42
}The filename must match the txid field: utxo/<txid>.json.
Computing a txid:
import hashlib
txid = hashlib.sha256(f"{owner}{amount}{created_at_block}".encode()).hexdigest()Follow these steps once when deploying a new GitCoin instance.
Create a new public GitHub repository. Do not enable Branch Protection yet.
python3 .github/scripts/generate_keypair.pyimport hashlib
owner = "YOUR_GITHUB_USERNAME"
amount = 4294967295
block_hash = "0" * 64
txid = hashlib.sha256(f"{owner}{amount}{block_hash}".encode()).hexdigest()
print(txid)genesis/genesis.json — replace REPLACE_WITH_FOUNDER_USERNAME and REPLACE_WITH_GENESIS_TXID.
validators/pubkeys.json — replace placeholders with your username and public key.
validators/registry.json — replace REPLACE_WITH_FOUNDER_USERNAME with your username.
Create utxo/<genesis_txid>.json:
{
"txid": "<your computed genesis txid>",
"owner": "YOUR_GITHUB_USERNAME",
"amount": 4294967295,
"unit": "GTC",
"created_at_block": "0000000000000000000000000000000000000000000000000000000000000000",
"created_at_height": 0
}git add .
git commit -m "genesis: initialize GitCoin ledger"
git push origin mainIn your repository Settings → Pages:
- Set Source to GitHub Actions
In Settings → Branches → Add rule for main:
- Require status checks to pass before merging
- Add required check:
validate-tx / validate - Add required check:
consensus-check / passed
- Add required check:
- Require branches to be up to date before merging
- Include administrators ← CRITICAL: do not skip this
- Allow auto-merge
- Allow force pushes — leave unchecked
- Allow deletions — leave unchecked
After enabling "Include administrators", even you cannot merge without going through consensus. This is intentional — it is the foundation of the system's trustlessness.
In your repository Issues → Labels, create:
tx-valid(color:#2ea043)tx-invalid(color:#f85149)tx-expired(color:#8b949e)
| Property | How it is enforced |
|---|---|
| No stored bot keys | All workflows use only ephemeral GITHUB_TOKEN (auto-issued per run, expires on completion) |
| No admin bypass | Branch Protection includes administrators |
| No double-spend | Git merge conflict blocks the second PR deleting the same UTXO file |
| No code injection from PR | pull_request_target runs main branch code; the PR head branch is never checked out or executed |
| No shell injection from PR body | PR body is parsed as plain text by Python, never interpolated into shell commands |
| Signature forgery | Ed25519 signatures are verified against the registered public key for each sender |
| Sybil validators | Public key registration required; key must be merged into main via consensus |
.github/
├── workflows/
│ ├── validate-tx.yml pull_request_target → verify TX sig; on success posts validator
│ │ vote comment inline (no separate assign-validators trigger needed)
│ │ non-TX PRs get immediate success on both required checks
│ ├── consensus-check.yml issue_comment → count /approve from pubkeys.json validators
│ ├── expire-tx.yml schedule (6h) → close PRs past 48h deadline
│ └── update-pages.yml push to main (utxo/** or validators/**) → rebuild + deploy Pages
└── scripts/
├── validate_tx.py Core validation logic (TRANSFER + REGISTER_KEY)
├── update_ledger.py UTXO scanner → ledger.json with balances, TX history, validators
├── generate_keypair.py User tool: generate Ed25519 keypair
└── create_transaction.py User tool: build, sign, write files, git rm inputs automatically
utxo/ One JSON file per unspent coin
validators/
pubkeys.json Ed25519 public keys per GitHub username (validator pool)
registry.json Optional scoring metadata (score, last_active)
genesis/
genesis.json Genesis block metadata
docs/
index.html Explorer UI: Balances / Transactions / Validators tabs
ledger.json Snapshot rebuilt after every merge to main
If GitHub ever becomes unavailable, the entire ledger history is preserved in every fork's git log. The same workflow logic can be migrated to:
- GitLab (GitLab CI/CD)
- Gitea / Forgejo (Gitea Actions)
- Radicle (decentralized git hosting)
The UTXO files and commit history are the canonical truth. No data lives outside the repository.