DEV Community

Cover image for Building an Onchain Reputation Graph for Open Source
Harish Kotra (he/him)
Harish Kotra (he/him)

Posted on

Building an Onchain Reputation Graph for Open Source

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"
}
Enter fullscreen mode Exit fullscreen mode

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'],
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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),
  }
)
Enter fullscreen mode Exit fullscreen mode

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)    │
└──────────┘                    └──────────────────┘
Enter fullscreen mode Exit fullscreen mode

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),
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What's Next

  • Intuition Mainnet — deploy predicate atoms and publish to production
  • Dependency Graph — publish dependsOn triples for npm dependency trees
  • Scheduled Re-ingestion — GitHub Action that re-ingests repos weekly to pick up new data
  • Reputation Leaderboard — a /leaderboard page on the explorer ranking contributors globally
  • Wallet-Connected Vouch — let anyone vouch for a contributor directly from the web UI

Links

Top comments (0)