Skip to main content
Glama
README.md
# ๐Ÿ” Soulprint

**Decentralized KYC identity protocol for AI agents.**

Soulprint lets any AI bot prove there's a verified human behind it โ€” without revealing who that human is. No companies, no servers, no paid APIs. Just cryptographic proof.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[[![npm soulprint](https://img.shields.io/npm/v/soulprint?label=soulprint&color=blue)](https://npmjs.com/package/soulprint)
[![npm soulprint-mcp](https://img.shields.io/npm/v/soulprint-mcp?label=soulprint-mcp&color=purple)](https://npmjs.com/package/soulprint-mcp)
![Phase](https://img.shields.io/badge/v0.3.9-phases%201--5%20%2B%20hardened-brightgreen-brightgreen)()
[![npm soulprint-network](https://img.shields.io/npm/v/soulprint-network?label=soulprint-network&color=7c6cf5)](https://npmjs.com/package/soulprint-network)[![Built with](https://img.shields.io/badge/built%20with-Circom%20%2B%20snarkjs%20%2B%20InsightFace-purple)]()

---

## The Problem

AI agents are acting on behalf of humans: booking flights, calling APIs, making decisions. But no service can know if a bot is legitimate or malicious. There's no accountability.

**Soulprint solves this** by linking every bot to a verified human identity โ€” cryptographically, privately, and without any central authority.

---

## How It Works

```
1. User runs: npx soulprint verify-me --selfie me.jpg --document cedula.jpg
              โ†“
2. LOCAL (on-device, nothing leaves your machine):
   โ€ข Tesseract OCR reads the cedula (Colombian ID)
   โ€ข InsightFace matches your face to the document photo
   โ€ข Poseidon hash derives a unique nullifier from (cedula + birthdate + face_key)
   โ€ข ZK proof generated: "I verified my identity" without revealing any data
   โ€ข Photos deleted from memory
              โ†“
3. ZK proof + SPT broadcast to validator node (verifies in 25ms, offline)
              โ†“
4. Soulprint Token (SPT) stored in ~/.soulprint/token.spt โ€” valid 24h
              โ†“
5. Any MCP server or API verifies in <50ms, offline, for free
```

**What the verifier knows:** โœ… Real human, verified Colombian ID, trust score  
**What the verifier doesn't know:** ๐Ÿ”’ Name, cedula number, face, birthdate

---

## Quick Start

### 1. Install Python deps (face recognition)

```bash
npx soulprint install-deps
```

### 2. Verify your identity

```bash
npx soulprint verify-me \
  --selfie path/to/selfie.jpg \
  --document path/to/cedula.jpg
```

Output:
```
๐Ÿ” Soulprint โ€” Verificaciรณn de identidad
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
  โœ… Validaciรณn de imรกgenes
  โœ… OCR del documento
  โœ… Coincidencia facial
  โœ… Derivaciรณn de nullifier
  โœ… Generaciรณn de ZK proof
  โœ… Emisiรณn del token SPT

  DID:          did:key:z6Mk...
  Trust Score:  45/100
  ZK Proof:     โœ… incluido
  Tiempo:       3.2s
```

### 3. Show your token

```bash
npx soulprint show
```

### 4. Renew (no re-verify needed)

```bash
npx soulprint renew
```

### 5. Run a validator node

```bash
npx soulprint node --port 4888
```

---

## Protect Any MCP Server (3 lines)

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { soulprint } from "soulprint-mcp";

const server = new McpServer({ name: "my-server", version: "1.0" });
server.use(soulprint({ minScore: 60 }));  // require KYC-verified humans
```

The client must include the SPT in capabilities:
```json
{
  "capabilities": {
    "identity": { "soulprint": "<token>" }
  }
}
```

Or in the HTTP header: `X-Soulprint: <token>`

### With DPoP โ€” prevent token theft (v0.3.8)

```typescript
// โ”€โ”€ Server side โ€” strict mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
server.use(soulprint({ minScore: 60, requireDPoP: true }));
// โ†’ 401 { error: "dpop_required" } if no proof header

// โ”€โ”€ Client side โ€” sign every request โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import { signDPoP, serializeDPoP } from "soulprint-core";

// Load your keypair (never transmit the private key)
const { privateKey, did } = loadKeypair();
const myToken = "<your-SPT>";

// Before each tool call:
const proof = signDPoP(privateKey, did, "POST", toolUrl, myToken);
headers["X-Soulprint"]       = myToken;
headers["X-Soulprint-Proof"] = serializeDPoP(proof);
```

A stolen SPT is **useless** without the private key. The proof is:
- Unique per request (random nonce)
- URL + method bound (no MITM)
- Expires in 5 minutes
- Hash-bound to the specific token

---

## Protect Any REST API

```typescript
import express from "express";
import { soulprint } from "soulprint-express";

const app = express();

// Protect entire API
app.use(soulprint({ minScore: 40 }));

// Strict: require DPoP proof (prevent token theft)
app.use(soulprint({ minScore: 65, requireDPoP: true }));

// Or specific routes
app.post("/sensitive", soulprint({ require: ["DocumentVerified", "FaceMatch"] }), handler);

// Access the verified identity + check if token was auto-renewed
app.get("/me", soulprint({ minScore: 20 }), (req, res) => {
  const renewedToken = res.getHeader("X-Soulprint-Token-Renewed");
  res.json({
    nullifier: req.soulprint!.nullifier,  // unique per human, no PII
    score:     req.soulprint!.score,
    ...(renewedToken ? { token_renewed: renewedToken } : {}),
  });
});
```

### Fastify

```typescript
import { soulprintFastify } from "soulprint-express";

await fastify.register(soulprintFastify, { minScore: 60 });

fastify.get("/me", async (request) => ({
  nullifier: request.soulprint?.nullifier,
}));
```

## Run a Validator Node

Anyone can run a validator node. Each node runs **two stacks simultaneously**: HTTP (port 4888) + libp2p P2P (port 6888).

```bash
# Arranque simple โ€” mDNS descubre nodos en la misma LAN automรกticamente
npx soulprint node

# Con bootstrap nodes para conectar a la red global
SOULPRINT_BOOTSTRAP=/ip4/x.x.x.x/tcp/6888/p2p/12D3KooW... \
npx soulprint node
```

Output esperado:
```
๐ŸŒ Soulprint Validator Node v0.2.2
   Node DID:     did:key:z6Mk...
   Listening:    http://0.0.0.0:4888

๐Ÿ”— P2P activo
   Peer ID:    12D3KooW...
   Multiaddrs: /ip4/x.x.x.x/tcp/6888/p2p/12D3KooW...
   Gossip:     HTTP fallback + GossipSub P2P
   Discovery:  mDNS (+ DHT si hay bootstraps)
```

Node API:
```
GET  /info              โ€” node info + p2p stats (peer_id, peers, multiaddrs)
POST /verify            โ€” verify ZK proof + co-sign SPT
POST /reputation/attest โ€” issue +1/-1 attestation (propagado via GossipSub)
GET  /reputation/:did   โ€” get bot reputation
GET  /nullifier/:hash   โ€” check anti-Sybil registry
```

---

## Architecture

```
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Layer 4 โ€” SDKs (soulprint-mcp, express)      โœ… Done  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Layer 3 โ€” Validator Nodes (HTTP + anti-Sybil)  โœ… Done โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Layer 2 โ€” ZK Proofs (Circom + snarkjs)         โœ… Done โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Layer 1 โ€” Local Verification (Face + OCR)      โœ… Done โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
```

### On-Demand ML Models

AI models are **never running persistently**:

```
Idle state:       ~8MB RAM   (only the CLI)
During verify:    ~200MB RAM (InsightFace subprocess spawned)
After verify:     ~8MB RAM   (subprocess exits โ†’ memory freed)
```

---

## Packages

| Package | Version | Description | Install |
|---|---|---|---|
| [`soulprint-core`](packages/core) | `0.1.6` | DID, SPT tokens, Poseidon nullifier, PROTOCOL constants, anti-farming | `npm i soulprint-core` |
| [`soulprint-verify`](packages/verify-local) | `0.1.4` | OCR + face match (on-demand), biometric thresholds from PROTOCOL | `npm i soulprint-verify` |
| [`soulprint-zkp`](packages/zkp) | `0.1.5` | Circom circuit + snarkjs prover, face_key via PROTOCOL.FACE_KEY_DIMS | `npm i soulprint-zkp` |
| [`soulprint-network`](packages/network) | `0.4.1` | Validator node: HTTP + P2P + credential validators + anti-farming | `npm i soulprint-network` |
| [`soulprint-mcp`](packages/mcp) | `0.1.5` | MCP middleware (3 lines) | `npm i soulprint-mcp` |
| [`soulprint-express`](packages/express) | `0.1.3` | Express/Fastify middleware | `npm i soulprint-express` |
| [`soulprint`](packages/cli) | `0.1.3` | `npx soulprint` CLI | `npm i -g soulprint` |

---

## ZK Circuit

The heart of Soulprint is a [Circom](https://circom.io) circuit that proves:

> *"I know a cedula number + birthdate + face key such that*  
> *`Poseidon(cedula, birthdate, face_key) == nullifier`*  
> *AND the cedula is within valid Registradurรญa ranges"*

Without revealing any of the private inputs.

**Circuit stats:**
- 844 non-linear constraints
- 4 private inputs (cedula, birthdate, face_key, salt)  
- 2 public inputs (nullifier, context_tag)
- Proof generation: ~600ms on a laptop
- Proof verification: ~25ms offline

---

## Soulprint Token (SPT)

A base64url-encoded signed JWT. **Contains no PII.**

```json
{
  "sip":         "1",
  "did":         "did:key:z6MkhaXgBZ...",
  "score":       45,
  "level":       "KYCFull",
  "country":     "CO",
  "credentials": ["DocumentVerified", "FaceMatch"],
  "nullifier":   "0x7090787188...",
  "zkp":         "eyJwIjp7InBpX2EiOlsi...",
  "issued":      1740000000,
  "expires":     1740086400,
  "sig":         "ed25519_signature"
}
```

---

## Trust Scoring

```
Credential          | Score
--------------------|-------
EmailVerified       | +10
PhoneVerified       | +15
GitHubLinked        | +20
DocumentVerified    | +25
FaceMatch           | +20
BiometricBound      | +10
                    |
KYCFull (doc+face)  |  45/100
```

Services choose their own threshold:
```typescript
soulprint({ minScore: 20 })   // email verified is enough
soulprint({ minScore: 45 })   // require doc + face KYC
soulprint({ minScore: 80 })   // require full biometric + extra
```

---

## Anti-Sybil Protection

The nullifier is derived from **biometric + document data**:

```
nullifier = Poseidon(cedula_number, birthdate, face_key)
face_key  = Poseidon(quantized_face_embedding[0..31])
```

- Same person, different device โ†’ **same nullifier**
- Different person, same cedula โ†’ **different nullifier** (face doesn't match)
- Person registers twice โ†’ nullifier already exists โ†’ **rejected by validator**

---

## Supported Countries

| Country | Document | Status |
|---|---|---|
| ๐Ÿ‡จ๐Ÿ‡ด Colombia | Cรฉdula de Ciudadanรญa (MRZ + OCR) | โœ… Supported |
| ๐ŸŒŽ Others | Passport (ICAO TD3 MRZ) | ๐Ÿšง Planned |

---

## Development Setup

```bash
git clone https://github.com/manuelariasfz/soulprint
cd soulprint
pnpm install
pnpm build
```

### Run integration tests

```bash
# ZK proof tests (no circuit compilation needed)
cd packages/zkp && node dist/prover.test.js

# Full integration tests
node -e "require('./packages/core/dist/index.js')"
```

### Compile ZK circuit (first time only)

```bash
pnpm --filter soulprint-zkp build:circuits
```

### Python dependencies

```bash
pip3 install insightface opencv-python-headless onnxruntime
```

---

## Trust Score โ€” 0 to 100

```
Total Score (0-100) = Identity (0-80) + Bot Reputation (0-20)
```

**Identity credentials (max 80 pts):**

| Credential | Points | How |
|---|---|---|
| EmailVerified | +8 | Email confirmation |
| PhoneVerified | +12 | SMS OTP |
| GitHubLinked | +16 | OAuth |
| DocumentVerified | +20 | OCR + MRZ (ICAO 9303) |
| FaceMatch | +16 | InsightFace biometric |
| BiometricBound | +8 | Device binding |

**Access levels:**

| Score | Level | Access |
|---|---|---|
| 0โ€“17 | Anonymous | Basic tools |
| 18โ€“59 | Partial KYC | Standard features |
| 60โ€“94 | KYCFull | Advanced features |
| **95โ€“100** | **KYCFull + reputation** | **Premium endpoints** |

---

## Bot Reputation (v0.1.3)

The reputation layer (0โ€“20 pts) builds over time from behavioral **attestations** issued by verified services.

```
Reputation starts at: 10 (neutral)
Verified service issues +1  โ†’  goes up  (max 20)
Verified service issues -1  โ†’  goes down (min 0)
```

**Attestation format (Ed25519 signed):**

```typescript
interface BotAttestation {
  issuer_did: string;  // service DID (requires score >= 60 to issue)
  target_did: string;  // bot being rated
  value:      1 | -1;
  context:    string;  // "spam-detected", "normal-usage", "payment-completed"
  timestamp:  number;
  sig:        string;  // Ed25519 โ€” bound to issuer_did
}
```

**Only services with score โ‰ฅ 60 can issue attestations.** This prevents low-quality services from gaming the network.

Attestations propagate **P2P across all validator nodes** via libp2p GossipSub (with HTTP fallback for legacy nodes).

---

## Anti-Farming Protection (v0.3.5)

The reputation system is protected against point farming. **Detected farming โ†’ automatic -1 penalty** (not just rejection).

Rules enforced by all validator nodes (`FARMING_RULES` โ€” `Object.freeze`):

| Rule | Limit |
|---|---|
| Daily gain cap | Max **+1 point/day** per DID |
| Weekly gain cap | Max **+2 points/week** per DID |
| New DID probation | DIDs < 7 days need **2+ existing attestations** before earning |
| Same-issuer cooldown | Max 1 reward/day from the same service |
| Session duration | Min **30 seconds** |
| Tool entropy | Min **4 distinct tools** used |
| Robotic pattern | Call interval stddev < 10% of mean โ†’ detected as bot |

```typescript
// Example: attacker trying to farm +1 every 60s
// Result: +1 โ†’ converted to -1 (automatic penalty)
POST /reputation/attest
{ did, value: 1, context: "normal-usage", session: { duration: 8000, tools: ["search","search","search"] } }
// โ†’ { value: -1, farming_detected: true, reason: "robotic-pattern" }
```

---

## Credential Validators (v0.3.5)

Every validator node ships with **3 open-source credential verifiers** โ€” no API keys required:

### ๐Ÿ“ง Email OTP (nodemailer)
```bash
POST /credentials/email/start   { did, email }
# โ†’ OTP sent to email (dev: Ethereal preview, prod: any SMTP)
POST /credentials/email/verify  { sessionId, otp }
# โ†’ issues credential:EmailVerified attestation, gossiped P2P
```

### ๐Ÿ“ฑ Phone TOTP (RFC 6238 โ€” no SMS, no API key)
```bash
POST /credentials/phone/start   { did, phone }
# โ†’ returns totpUri โ€” scan with Google Authenticator / Authy / Aegis
POST /credentials/phone/verify  { sessionId, code }
# โ†’ issues credential:PhoneVerified attestation
```

### ๐Ÿ™ GitHub OAuth (native fetch)
```bash
GET /credentials/github/start?did=...
# โ†’ redirects to github.com OAuth
GET /credentials/github/callback
# โ†’ issues credential:GitHubLinked attestation with github.login
```
Config: `GITHUB_CLIENT_ID` + `GITHUB_CLIENT_SECRET` + `SOULPRINT_BASE_URL`

---

## Protocol Constants (v0.3.5)

All critical values are **immutable at runtime** via `Object.freeze()` in `soulprint-core`. Changing them requires a new SIP (Soulprint Improvement Proposal) and a protocol version bump.

```typescript
import { PROTOCOL } from 'soulprint-core';

PROTOCOL.FACE_SIM_DOC_SELFIE    // 0.35 โ€” min similarity document vs selfie
PROTOCOL.FACE_SIM_SELFIE_SELFIE // 0.65 โ€” min similarity selfie vs selfie (liveness)
PROTOCOL.FACE_KEY_DIMS          // 32   โ€” embedding dimensions for face_key
PROTOCOL.FACE_KEY_PRECISION     // 1    โ€” decimal precision (absorbs ยฑ0.01 noise)
PROTOCOL.SCORE_FLOOR            // 65   โ€” minimum score any service can require
PROTOCOL.VERIFIED_SCORE_FLOOR   // 52   โ€” floor for DocumentVerified identities
PROTOCOL.MIN_ATTESTER_SCORE     // 65   โ€” minimum score to issue attestations
PROTOCOL.VERIFY_RETRY_MAX       // 3    โ€” max retries for remote verification
```

> These constants are **write-protected** โ€” `PROTOCOL.FACE_SIM_DOC_SELFIE = 0.1` throws at runtime.

---

## Live Ecosystem โ€” mcp-colombia-hub

[mcp-colombia-hub](https://github.com/manuelariasfz/mcp-colombia) is the **first verified service** in the Soulprint ecosystem:

- **Service score:** 80 (DocumentVerified + FaceMatch + GitHubLinked + BiometricBound)
- **Auto-issues -1** when a bot spams (>5 req/60s)
- **Auto-issues +1** when a bot completes 3+ tools normally
- **Premium endpoint `trabajo_aplicar`** requires score โ‰ฅ 40

```bash
npx -y mcp-colombia-hub
```

---

## Security Model

| Threat | Defense |
|---|---|
| Someone learns your DID | DID is public โ€” harmless without private key |
| Private key theft | Key lives in `~/.soulprint/` (mode 0600) |
| Fake cedula image | Face match required |
| Register twice | Nullifier uniqueness on validator network |
| Replay attack | Token expires in 24h + context_tag per service |
| Sybil attack | Biometric nullifier โ€” same face = same nullifier |
| DID substitution attack | Ed25519 signature bound to DID keypair |

---

## Roadmap

```
โœ… Phase 1 โ€” Local verification (cedula OCR + face match + nullifier)
โœ… Phase 2 โ€” ZK proofs (Circom circuit + snarkjs prover/verifier)
โœ… Phase 3 โ€” Validator nodes (HTTP + ZK verify + anti-Sybil registry)
โœ… Phase 4 โ€” SDKs (soulprint-mcp, soulprint-express)
โœ… Phase 5 โ€” P2P network (libp2p v2 ยท Kademlia DHT + GossipSub + mDNS ยท soulprint-network@0.2.2)
โœ… v0.3.7 โ€” Challenge-Response peer integrity ยท snarkjs critical fix ยท SPT auto-renewal
โœ… v0.3.5 โ€” Anti-farming engine ยท Credential validators (email/phone/GitHub) ยท Biometric PROTOCOL constants
๐Ÿšง Phase 6 โ€” Multi-country support (passport, DNI, CURP, RUT...)
๐Ÿ”ฎ Phase 7 โ€” On-chain nullifier registry (optional, EVM-compatible)
```

---


### Phase 5f โ€” Auto-Renewal of SPT (v0.3.6) โœ…

SPTs (Soulprint Protocol Tokens) now renew automatically โ€” no more downtime when a 24-hour token expires.

#### How it works

```
[Bot SDK] โ”€โ”€detects near-expiryโ”€โ”€โ–บ POST /token/renew โ”€โ”€โ–บ [Validator Node]
                                        โ†‘ current SPT           โ†“ fresh SPT (24h)
[Middleware] โ—„โ”€โ”€โ”€ X-Soulprint-Token-Renewed: <new_spt> โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
```

**Renewal windows:**
| Scenario | Window | Action |
|---|---|---|
| Token valid, < 1h remaining | Pre-emptive | Auto-renew |
| Token expired < 7 days ago | Grace period | Auto-renew |
| Token expired > 7 days ago | Stale | Full re-verification required |

#### Validator endpoint

```bash
POST /token/renew
Body: { "spt": "<current_token>" }

Response 200: {
  "spt": "<new_token>",
  "expires_in": 86400,
  "renewed": true,
  "method": "preemptive" | "grace_window"
}
```

#### Express middleware (automatic)

```typescript
import { soulprint } from "soulprint-express";

app.use(soulprint({
  minScore: 40,
  nodeUrl: "https://validator.soulprint.digital",  // enables auto-renew
}));

// New token arrives in response header if renewed:
// X-Soulprint-Token-Renewed: <new_spt>
// X-Soulprint-Expires-In: 86400
```

#### MCP middleware (automatic)

```typescript
import { requireSoulprint } from "soulprint-mcp";

server.use(requireSoulprint({
  minScore: 65,
  nodeUrl: "https://validator.soulprint.digital",
}));
// Renewed token propagated in context.meta["x-soulprint-token-renewed"]
```

#### Manual (any SDK)

```typescript
import { autoRenew, needsRenewal } from "soulprint-core";

const check = needsRenewal(currentSpt);
if (check.needsRenew) {
  const { spt, renewed } = await autoRenew(currentSpt, { nodeUrl });
  if (renewed) saveSpt(spt);  // persist the new token
}
```


### Phase 5g โ€” Challenge-Response Peer Integrity + snarkjs Fix (v0.3.7) โœ…

#### Critical bug fix โ€” `soulprint-zkp@0.1.5`

`verifyProof()` was silently broken since v0.1.0. The snarkjs CJS module has `__esModule: true` but no `.default` property โ€” TypeScript's `__importDefault` returned the module as-is, then code accessed `.default.groth16` which was `undefined`. All ZK proof verifications crashed at runtime.

```typescript
// โŒ Before (broken):
import snarkjs from "snarkjs";          // compiles to snarkjs_1.default.groth16 โ†’ undefined

// โœ… After (fixed):
import * as snarkjs from "snarkjs";     // compiles to snarkjs.groth16 โœ…
```

#### Challenge-Response Protocol (`soulprint-network@0.3.7`)

Peers now cryptographically verify that remote nodes are running **unmodified ZK verification code** before accepting them into the network.

```
Challenger                          Peer
    โ”‚                                 โ”‚
    โ”‚โ”€โ”€ POST /challenge โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚
    โ”‚   {challenge_id, nonce,         โ”‚
    โ”‚    valid_proof,                 โ”‚  verifyProof(valid_proof)   โ†’ true
    โ”‚    invalid_proof}               โ”‚  verifyProof(invalid_proof) โ†’ false
    โ”‚                                 โ”‚  sign(results, node_key)
    โ”‚โ—„โ”€โ”€ {result_valid: true, โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
    โ”‚     result_invalid: false,      โ”‚
    โ”‚     signature: Ed25519(...)}    โ”‚
    โ”‚                                 โ”‚
    โ”‚  verify signature โœ…            โ”‚
    โ”‚  result_valid == true โœ…        โ”‚
    โ”‚  result_invalid == false โœ…     โ”‚
    โ”‚                                 โ”‚
    โ”‚  โ†’ PEER ACCEPTED                โ”‚
```

**Attacks blocked:**

| Attack | Detection |
|--------|-----------|
| ZK always returns `true` (bypass) | `invalid_proof` must return `false` |
| ZK always returns `false` (broken) | `valid_proof` must return `true` |
| Pre-computed / cached response | Fresh random `nonce` makes `invalid_proof` unique per challenge |
| Node impersonation | Ed25519 signature tied to `node_did` |
| Replay attack | 30-second TTL on challenges |

**Invalid proof generation** โ€” the challenger mutates the valid proof with a random nonce:
```
invalid_proof.pi_a[0] = (valid_proof.pi_a[0] + nonce) mod p
```
This produces a cryptographically invalid proof that snarkjs will always reject โ€” but it's unpredictable without the nonce.

**Automatic peer verification** โ€” `POST /peers/register` now runs `verifyPeerBehavior()` before accepting any peer. A peer with modified ZK code is rejected with HTTP 403.

### Phase 5h โ€” DPoP: Demonstrating Proof of Possession (v0.3.8) โœ…

SPT tokens are bearer tokens โ€” stolen tokens could be used until expiry (24h). **DPoP** closes this window by requiring a fresh cryptographic proof with every request.

```
Without DPoP:  stolen SPT โ†’ attacker calls API โ†’ SUCCESS โœ—
With DPoP:     stolen SPT โ†’ attacker has no private key โ†’ 401 โœ“
```

**How it works:**

Every request carries `X-Soulprint-Proof` โ€” a payload signed with the user's Ed25519 private key:

```typescript
{
  typ:      "soulprint-dpop",
  method:   "POST",           // HTTP method โ€” bound
  url:      "https://...",    // exact URL โ€” bound
  nonce:    "a3f1b2...",      // 16 random bytes โ€” unique per request
  iat:      1740000000,       // expires in 5 minutes
  spt_hash: sha256(spt),      // bound to THIS specific token
}
// Signed: Ed25519(sha256(JSON.stringify(payload)), privateKey)
```

**Attacks blocked (8):** token theft, replay, URL MITM, method MITM, DID mismatch, expired proof, malformed proof, foreign token reuse.

**API:**
```typescript
import { signDPoP, verifyDPoP, serializeDPoP, NonceStore } from "soulprint-core";

const proof  = signDPoP(privateKey, did, "POST", url, spt);
const header = serializeDPoP(proof);  // base64url string โ†’ X-Soulprint-Proof

const result = verifyDPoP(header, spt, "POST", url, nonceStore, sptDid);
// result.valid โ†’ bool | result.reason โ†’ string
```

---

### Phase 5i โ€” MCPRegistry: Verified MCP Ecosystem (v0.3.9) โœ…

A public on-chain registry of verified MCP servers. Agents can check whether a server is legitimate before trusting it.

**Contract:** `MCPRegistry.sol` on Base Sepolia  
**Address:** `0x59EA3c8f60ecbAe22B4c323A8dDc2b0BCd9D3C2a`  
**Admin:** Soulprint Protocol (not any individual MCP)

```
Unverified MCP:  agent connects โ†’ no guarantee โ†’ risk โœ—
Verified MCP:    isVerified(0x...) โ†’ true on-chain โ†’ trusted โœ“
```

**Registration flow:**
```bash
# 1. Any dev registers their MCP (permissionless)
curl -X POST http://soulprint-node/admin/mcp/register \
  -d '{ "ownerKey": "0x...", "address": "0x...",
        "name": "My Finance MCP", "url": "https://...", "category": "finance" }'

# 2. Soulprint admin reviews and verifies
curl -X POST http://soulprint-node/admin/mcp/verify \
  -H "Authorization: Bearer ADMIN_TOKEN" \
  -d '{ "address": "0x..." }'
# โ†’ on-chain tx โ†’ MCPVerified event โ†’ permanent record

# 3. Anyone checks
curl http://soulprint-node/mcps/verified
# โ†’ [{ name: "My Finance MCP", badge: "โœ… VERIFIED", verified_at: "..." }]
```

**Check from code:**
```typescript
import { isVerifiedOnChain, getMCPEntry } from "soulprint-network";

const trusted = await isVerifiedOnChain("0x...");  // โ†’ true/false, on-chain

const entry = await getMCPEntry("0x...");
// โ†’ { name, url, category, verified, verified_at, badge: "โœ… VERIFIED by Soulprint" }
```

**Architectural separation:**
```
Soulprint validator = protocol authority โ†’ admin endpoints (verify/revoke)
Individual MCPs     = participants โ†’ read-only (check status, list verified)
MCPRegistry.sol     = source of truth โ†’ on-chain, immutable, auditable
```

---

### Phase 5j โ€” ProtocolThresholds: Mutable On-Chain Governance (v0.4.1) โœ…

Protocol thresholds (SCORE_FLOOR, VERIFIED_SCORE_FLOOR, FACE_SIM_*, etc.) now live on-chain in `ProtocolThresholds.sol` instead of being hardcoded.

**Contract (Base Sepolia):** `0xD8f78d65b35806101672A49801b57F743f2D2ab1`

```solidity
// Anyone can read
getThreshold("SCORE_FLOOR")         // โ†’ 65
getThreshold("FACE_SIM_DOC_SELFIE") // โ†’ 350 (= 0.35)
getAll()                            // โ†’ all 9 thresholds

// Only superAdmin can write
setThreshold("SCORE_FLOOR", 70)     // emits ThresholdUpdated event

// Admin transfer (2-step safety)
proposeSuperAdmin(addr) โ†’ acceptSuperAdmin()
```

**Validator integration:**
- Node loads thresholds from blockchain at startup (non-blocking, fallback to local if RPC unreachable)
- Auto-refresh every 10 minutes
- New endpoint: `GET /protocol/thresholds`

```json
{
  "source": "blockchain",
  "contract": "0xD8f78d65b35806101672A49801b57F743f2D2ab1",
  "thresholds": {
    "SCORE_FLOOR": 65,
    "VERIFIED_SCORE_FLOOR": 52,
    "MIN_ATTESTER_SCORE": 65,
    "FACE_SIM_DOC_SELFIE": 0.35,
    "FACE_SIM_SELFIE_SELFIE": 0.65,
    "DEFAULT_REPUTATION": 10,
    "IDENTITY_MAX": 80,
    "REPUTATION_MAX": 20
  }
}
```

**Tests:** 17/17 real flow tests on Base Sepolia (`tests/protocol-thresholds-tests.mjs`)

---

## Protocol Spec

See [specs/SIP-v0.1.md](specs/SIP-v0.1.md) for the Soulprint Identity Protocol specification.

---

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). All countries welcome โ€” add your ID document format in `packages/verify-local/src/document/`.

---

## License

MIT โ€” free for personal and commercial use.

---

*Built for the age of AI agents. Every bot has a soul behind it.*