The Problem
Open source contributions are scattered. Alice might contribute to 30 repos on GitHub, maintain 5 npm packages, review PRs on GitLab, and write documentation on a personal blog. There is no unified, portable, platform-agnostic way to answer a simple question: who built this?
Traditional solutions rely on centralized databases — Dune dashboards, custom scrapers, GitHub's own API. These are:
- Fragile — break when APIs change
- Non-portable — tied to a single platform's data model
- Not verifiable — anyone can claim anything without onchain proof
What if we could make OSS reputation deterministic, verifiable, and composable — like the contributions themselves?
The Approach
Atoms + Triples = A Knowledge Graph
The Intuition protocol models the world as atoms (entities) and triples (relationships). An atom is a unique identifier for anything — a person, a repo, an npm package. A triple is a directed edge connecting two atoms via a predicate.
// This triple says: "fisker contributedTo prettier/prettier"
{
subjectId: "0xabc...", // deterministic atom ID for GitHub user "fisker"
predicateId: "0xdef...", // atom ID for the "contributedTo" predicate
objectId: "0x123...", // deterministic atom ID for "prettier/prettier"
}
Deterministic IDs — The Key Insight
Atom IDs are deterministic. Given the same input data, you always get the same 32-byte identifier — no onchain lookup required.
import { buildAtom } from '@0xintuition/primitives'
// This always produces the same ID, anywhere, anytime
const person = buildAtom('person', {
givenName: 'fisker',
familyName: 'fisker',
sameAs: ['https://github.com/fisker'],
})
// person.id → 0xABC... (deterministic!)
// Same for repos
const repo = buildAtom('software', {
name: 'prettier',
codeRepository: 'https://github.com/prettier/prettier',
sameAs: ['https://github.com/prettier/prettier'],
})
This means two different applications, running on different machines, will derive the exact same atom ID for the same GitHub user. No coordination needed. No central registry.
The Pipeline
The ingest pipeline has six stages:
Stage 1: Fetch & Normalize
├── GitHub REST API → repo metadata, contributors, PRs, issues
└── npm Registry API → package name, version
Stage 2: Canonicalize & Build Atoms
├── Strip tracking params from URLs
├── Resolve canonical GitHub/npm URLs
├── Build sameAs arrays (platform-agnostic identity)
└── Create DerivedAtom objects for every entity
Stage 3: Derive Atom IDs (offchain, free)
└── deterministic IDs from canonicalized data
Stage 4: Deduplicate Against Graph
└── Check local registry + onchain for existing atoms
Stage 5: Build Triple Graph
└── Connect entities using predicate vocabulary
Stage 6: Publish (opt-in, with --publish flag)
└── Batch write to Intuition MultiVault contract
Technical Deep Dive
Onchain Publishing
When you run with --publish, the tool connects to the Intuition testnet via viem, fetches the current atom creation cost from the MultiVault contract, and publishes in batches:
import { multiVaultCreateAtoms, multiVaultGetAtomCost } from '@0xintuition/protocol'
import { getMultiVaultAddressFromChainId } from '@0xintuition/deployments'
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { intuitionTestnet } from '@0xintuition/deployments'
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
const walletClient = createWalletClient({ account, chain: intuitionTestnet, transport: http(rpcUrl) })
const publicClient = createPublicClient({ chain: intuitionTestnet, transport: http(rpcUrl) })
const multiVaultAddress = getMultiVaultAddressFromChainId(13579)
const atomCost = await multiVaultGetAtomCost({ address: multiVaultAddress, publicClient })
// Each batch sends exactly sum(assets) as msg.value
const txHash = await multiVaultCreateAtoms(
{ address: multiVaultAddress, walletClient, publicClient },
{
args: [hexDataArray, assetsArray], // atoms data + deposit amounts
value: atomCost * BigInt(batch.length),
}
)
The tool handles several edge cases gracefully:
-
MultiVault_AtomExists— atom already onchain, skip and retry -
MultiVault_TermDoesNotExist— a referenced atom doesn't exist yet, skip that triple -
Insufficient balance— wallet runs out mid-batch, publish what fits
Triple Graph
Triples connect atoms into a graph:
┌──────────┐ contributedTo ┌──────────────────┐
│ Person │ ─────────────────→ │ Repository │
│ (fisker) │ │ (prettier/prettier)│
└──────────┘ └──────────────────┘
│ │
│ authored │ maintainedBy
▼ ▼
┌──────────┐ ┌──────────────────┐
│ Article │ │ Person │
│ (issue/PR)│ │ (maintainer) │
└──────────┘ └──────────────────┘
Each relationship type is a custom predicate — itself an atom onchain:
| Predicate | Subject | Object | Semantic |
|---|---|---|---|
contributedTo |
Person | Repo | Person made at least one contribution |
authored |
Person | Article | Person created the issue or PR |
mergedBy |
Person | Article | Person merged the PR |
maintainedBy |
Repo | Person | Repo lists person as maintainer |
hasPackage |
Repo | SoftwareApp | Repo publishes an npm package |
worksAt |
Org | Person | Person is affiliated with the org |
Reputation Scoring — All Local
Reputation scores are computed entirely from the local graph — zero onchain reads required:
function computeReputation(githubHandle: string): ContributorReputation {
const atom = lookupByGithubHandle(githubHandle)
const triples = getAllTriples()
return {
commitDepth: countAuthored(atom, triples),
projectDiversity: countUniqueRepos(atom, triples),
maintainerTrust: countVouches(atom, triples),
dependencyReach: countDownstreamDeps(atom, triples),
longevity: daysSinceFirstContribution(atom, triples),
}
}
The Web Explorer
The web/ directory contains a read-only explorer — zero frameworks, zero build step, vanilla HTML/CSS/JS:
https://who-built-this.vercel.app
It queries the Intuition GraphQL API (https://testnet.intuition.sh/v1/graphql) and displays only atoms/triples created by the project's wallet. Click any atom to drill into its relationships.
Key design decisions:
- No wallet connection — the explorer is purely informational
- Filtered by creator — only shows data published by this tool
- Static deploy — single HTML file, deployable to Vercel, Netlify, or any static host
- Live status — green dot when data is indexed, amber when waiting for the indexer
Project Structure
who-built-this/
├── index.ts # Entry point
├── src/
│ ├── index.ts # CLI dispatcher
│ ├── config.ts # Environment + constants
│ ├── types.ts # TypeScript interfaces
│ ├── cli/commands.ts # CLI command handlers
│ ├── ingest/
│ │ ├── pipeline.ts # 6-stage ingestion pipeline
│ │ ├── github.ts # GitHub REST API client
│ │ └── npm.ts # npm Registry client
│ ├── atoms/builder.ts # Atom construction
│ ├── graph/
│ │ ├── onchain.ts # MultiVault publishing + retry logic
│ │ ├── registry.ts # Local JSON-backed registry
│ │ └── dedup.ts # Onchain deduplication
│ ├── predicates/vocabulary.ts # Predicate definitions
│ ├── reputation/scoring.ts # Local reputation computation
│ └── utils/canonicalize.ts # URL canonicalization
├── web/index.html # Read-only explorer (Vercel)
├── vercel.json # Vercel deployment config
└── package.json
Challenges & Lessons Learned
1. The MultiVault Cost Calculation
The initial implementation sent only the deposit amount as msg.value, but the Intuition MultiVault contract charges a base atom creation cost on top. The fix required fetching multiVaultGetAtomCost() and including it in both the assets array and the value.
// ❌ Wrong — missing the atom cost
const totalValue = ATOM_DEPOSIT * BigInt(batch.length)
// ✅ Correct — atom cost includes the base deposit
const atomCost = await multiVaultGetAtomCost(config)
const totalValue = atomCost * BigInt(batch.length)
2. Graceful Error Handling
The contract can revert for several reasons — atoms that already exist, triples referencing nonexistent atoms, insufficient balance mid-batch. The tool now handles each case:
// Retry loop removes failing items one at a time
while (pending.length > 0) {
try {
await multiVaultCreateAtoms(config, { args: [data, assets], value: totalValue })
break // success
} catch (err) {
if (msg.includes('MultiVault_AtomExists')) {
// Find which atom, skip it, retry the rest
} else if (msg.includes('MultiVault_TermDoesNotExist')) {
// Find which triple, skip it, retry the rest
}
}
}
3. GraphQL Indexer Latency
Transactions appear on the RPC immediately (status 0x1) but the Hasura-powered GraphQL API can lag by minutes or longer on testnet. The explorer displays a live status indicator to communicate this.
How to Contribute
The repo is at github.com/harishkotra/who-built-this.
Quick Ideas
| Feature | Difficulty | What's Involved |
|---|---|---|
| GitLab support | Medium | Add GitLab API client alongside GitHub client |
| npm dependents | Medium | Fetch downstream dependents, publish dependsOn triples |
| GitHub Action | Easy | Auto-publish repo metadata on push |
| Wallet vouch | Medium | Add MetaMask integration to the web explorer |
| Leaderboard | Medium | Aggregate all published data via GraphQL API |
Setup
git clone https://github.com/harishkotra/who-built-this.git
cd who-built-this
npm install
cp .env.example .env # add GITHUB_TOKEN for better rate limits
npm run ingest -- --repo prettier/prettier
What's Next
- Intuition Mainnet — deploy predicate atoms and publish to production
-
Dependency Graph — publish
dependsOntriples for npm dependency trees - Scheduled Re-ingestion — GitHub Action that re-ingests repos weekly to pick up new data
-
Reputation Leaderboard — a
/leaderboardpage on the explorer ranking contributors globally - Wallet-Connected Vouch — let anyone vouch for a contributor directly from the web UI
Links
- GitHub: github.com/harishkotra/who-built-this
- Live Explorer: who-built-this.vercel.app
- Intuition Protocol: intuition.systems
- Intuition Testnet Explorer: testnet.explorer.intuition.systems
- Intuition GraphQL API: testnet.intuition.sh/v1/graphql
Top comments (0)