Backend for GitBond — treasury, stablecoin and tokenized-asset operations driven from GitHub workflows. Serverless Python functions on Vercel, Postgres (Supabase) for state, wallet signature for auth.
This repository holds the backend and the wallet sign-in script. The marketing site is deployed separately.
| Area | State |
|---|---|
| 45 HTTP endpoints | implemented, unit-tested against a mocked database |
| Postgres schema — 34 tables, 23 views | 13 migrations + seed |
| Double-entry ledger | enforced by triggers, not by application code |
| Wallet sign-in (EIP-4361) | implemented, secp256k1 recovery written from scratch, no dependencies |
| Group chat + SSE stream | implemented, windowed to fit the function time limit |
| Autobranch code generation | not functional — see Limitations |
| On-chain execution | not implemented by design — see Limitations |
Tests: tests/ — five suites, all green. They run against an in-memory double of PostgREST
that also enforces the database triggers, so a balance rule broken in SQL fails the Python test.
Balances are derived, never stored. There is no balance column on a vault, an account or a project. A balance is the sum of confirmed entries in the ledger. Money moves only by inserting a transaction header, then entries that sum to zero per asset, then flipping the header to confirmed. Four triggers guard this: entries are append-only, nothing can be added to a confirmed transaction, an unbalanced transaction fails at COMMIT, and no internal account may go negative. Only one synthetic account — the one modelling the outside world — is allowed a negative balance, so that a deposit from outside still nets to zero.
These are triggers and CHECK constraints rather than row-level security, because RLS is bypassed by the service key while triggers are not. Even with full database access, the only way to change a balance is to write a balanced, immutable transaction that stays in the history.
Idempotency. Endpoints that move money accept a request twice without moving it twice. Where the client sends an operation token, the guarantee is permanent — a repeat returns the original transaction hash. Where it does not, a content key over the parameters plus a unique index catches both retries and races. A partially applied transaction resumes rather than restarts.
Server-Sent Events on serverless. A function cannot hold a connection open indefinitely, so
/api/group/stream serves a bounded window and closes itself before the platform would cut it
mid-frame. Clients reconnect and catch up through a monotonic cursor over an outbox table, so
nothing is lost across the gap.
Stated plainly, because they are deliberate.
No blockchain. Vault operations are accounting entries. Addresses and transaction hashes are
derived deterministically from the operation and stored as strings; nothing is broadcast to any
network. GET /api/vault/key answers 410 Gone — no key material is stored anywhere in the
schema, so there is nothing for it to return.
Autobranch cannot write to GitHub. POST /api/autogit/push and /deploy answer 501. They
would need a GitHub App with write access, which this deployment does not have. They return an
error rather than a plausible-looking repository URL.
No model behind the assistant. /api/autogit/chat and /improve say so instead of pretending
to have edited files.
Statistics carry over. /api/stats includes counters from an earlier deployment whose history
does not exist in this database. They live in a single stats_carryover row rather than being
faked as individual records, and every other number is computed from real rows.
Requires a Supabase project and a Vercel deployment.
# 1. apply the schema
psql "$DATABASE_URL" -f db/migrations/001_foundation.sql # ... through 013
psql "$DATABASE_URL" -f db/seed.sql
# 2. environment variables on Vercel
SUPABASE_URL # Supabase → Settings → Data API
SUPABASE_SERVICE_KEY # Supabase → Settings → API keys → service_role
SESSION_SECRET # python3 -c "import secrets; print(secrets.token_urlsafe(48))"
GROUP_ADMIN_SECRET # any long random string
SIWE_DOMAIN # the domain the site is served from
# 3. tests
python3 tests/test_auth.py
python3 tests/test_vault.py
python3 tests/test_group.py
python3 tests/test_public_api.py
python3 tests/smoke_test.pyEvery missing variable produces a 503 naming the variable, never a silent failure.
requirements.txt is intentionally empty — the shared library is standard-library only, so a
broken dependency cannot take down every function at once.
api/
_lib/ shared helpers — responses, errors, auth, Supabase client, address derivation
auth/ EIP-4361 sign-in: nonce, verify, me, logout
vault/ treasury operations, all of them ledger writes
group/ chat, moderation, SSE stream
autogit/ Autobranch endpoints
... stats, tokens, prices, transactions, projects, repos, rwa
db/
migrations/ 13 files, ordered
seed.sql reference data
SCHEMA.md diagram, table notes, endpoint-to-table map
docs/
API_SPEC.md recovered contract for all 54 endpoints the frontend calls
tests/ five suites
web/
gitbond-wallet.js wallet sign-in for the static frontend
MIT