๐Ÿ“ก REST API Reference

All endpoints exposed by a running Soulprint validator node (default: http://localhost:4888).

GET /info

Returns node status and network statistics. No authentication required.

Response

{
  "peerId":             "12D3KooW...",
  "version":            "0.2.2",
  "httpPort":           4888,
  "p2pPort":            6888,
  "connectedPeers":     3,
  "attestationsIssued": 142,
  "proofHashesStored":  891,
  "uptime":             86400,
  "multiaddrs": ["/ip4/0.0.0.0/tcp/6888/p2p/12D3KooW..."]
}

POST /verify

Verifies a Soulprint token and registers its proof hash for anti-replay. Returns the decoded token payload on success.

Request Body

{ "token": "<SPT token string>" }

Response (200 OK)

{
  "valid":        true,
  "did":          "did:soulprint:abc123...",
  "score":        72,
  "identity":    62,
  "reputation":  10,
  "country":     "CO",
  "credentials": ["email", "document", "face_match"],
  "expiresAt":   "2026-08-24T10:00:00Z",
  "firstSeen":   "2026-02-24T10:00:00Z"
}

Response (400 โ€” expired or invalid)

{ "valid": false, "error": "token expired" }

Response (409 โ€” replay detected)

{ "valid": false, "error": "proof hash already registered (Sybil attempt)" }

POST /reputation/attest

Issue a behavioral attestation for a DID. Requires operator authorization.

Request Body

{
  "did":        "did:soulprint:abc123...",
  "delta":      1,              // +1 (positive) or -1 (negative)
  "reason":    "diverse_tool_use",  // free-form label for audit log
  "operatorSig": "<signature>"   // signed by operator key
}

Response (200 OK)

{
  "ok":        true,
  "did":       "did:soulprint:abc123...",
  "newScore": 11,
  "attestationId": "attest-uuid-..."
}

GET /reputation/:did

Returns the current bot reputation score for a DID.

Response

{
  "did":            "did:soulprint:abc123...",
  "score":          12,
  "attestations":  4,        // total attestations received
  "positive":      3,
  "negative":      1,
  "lastUpdated":   "2026-02-24T14:00:00Z"
}

GET /proof-hash/:hash

Checks if a proof hash has been registered (anti-replay lookup).

Response

{
  "hash":       "sha3-abc123...",
  "registered": true,
  "firstSeen":  "2026-02-24T10:00:00Z",
  "did":        "did:soulprint:abc123..."   // included if registered
}

Credential Validators v0.3.0

POST /credentials/email/start

Send a 6-digit OTP to an email address.

// Request
{ "did": "did:soulprint:...", "email": "user@example.com" }
// Response
{ "sessionId": "abc123...", "message": "OTP sent", "preview": "https://ethereal.email/..." }

POST /credentials/email/verify

{ "sessionId": "abc123...", "otp": "123456" }
// Response
{ "credential": "EmailVerified", "did": "...", "attestation": { ... } }

POST /credentials/phone/start

Get a TOTP URI to scan with Google Authenticator / Authy / Aegis. No SMS.

{ "did": "...", "phone": "+573001234567" }
// Response
{ "sessionId": "...", "totpUri": "otpauth://totp/Soulprint:...", "instructions": "Scan QR..." }

POST /credentials/phone/verify

{ "sessionId": "...", "code": "179941" }
// Response
{ "credential": "PhoneVerified", "did": "...", "attestation": { ... } }

GET /credentials/github/start?did=...

Redirect to GitHub OAuth. Requires GITHUB_CLIENT_ID configured.

GET /credentials/github/callback

OAuth callback. Issues GitHubLinked attestation after successful auth.

{ "credential": "GitHubLinked", "did": "...", "github": { "login": "manuelariasfz" }, "attestation": { ... } }

Error Codes

HTTPErrorMeaning
400invalid_tokenToken is malformed or signature invalid
400token_expiredToken has passed its expiresAt date
401unauthorizedMissing or invalid operator signature for attestation endpoints
409replay_detectedProof hash already registered โ€” Sybil attempt blocked
429rate_limitedToo many requests โ€” retry after Retry-After header seconds
503node_syncingNode is still syncing with the P2P network on startup

Rate Limits

EndpointDefault limitNotes
GET /infoUnlimitedHealth check endpoint
POST /verify100/min per IPConfigurable via SOULPRINT_RATE_LIMIT
POST /reputation/attest60/min per operator keyRequires valid operator signature
GET /reputation/:did200/min per IPRead-only, cached
GET /proof-hash/:hash200/min per IPRead-only, cached

Blockchain Anchor

GET /anchor/stats

Returns the status of the async blockchain backup.

GET /anchor/stats

Response:
{
  "nullifiersAnchored":  12,       // total anchored to blockchain
  "attestsAnchored":     47,
  "pendingNullifiers":   0,        // in retry queue
  "pendingAttests":      0,
  "blockchainConnected": true,     // false = P2P-only mode
  "lastAnchorTs":        1740000000000
}

Governance

On-chain governance for PROTOCOL_HASH upgrades via GovernanceModule.sol.

GET /governance

{
  "currentApprovedHash": "0xdfe1ccca...",
  "blockchainConnected": true,
  "activeProposals": 0,
  "hashHistory": ["0xdfe1ccca..."],
  "nodeCompatible": true
}

GET /governance/proposals

List all active or approved-pending-timelock proposals.

GET /governance/proposal/:id

Full proposal detail including timelockRemainingSeconds.

POST /governance/propose

{ "did": "did:key:z6Mk...", "newHash": "0x...", "rationale": "..." }
โ†’ { "txHash": "0x...", "proposalId": 0 }

POST /governance/vote

{ "proposalId": 0, "did": "did:key:z6Mk...", "approve": true }
โ†’ { "txHash": "0x...", "proposalId": 0, "approve": true }

POST /governance/execute

{ "proposalId": 0 }
โ†’ { "txHash": "0x...", "proposalId": 0, "executed": true }

POST /challenge v0.3.7

Peer integrity check via ZK challenge-response. Verifies that the peer is running unmodified ZK verification code.

POST /challenge
Content-Type: application/json

{
  "challenge_id": "a3f1b2c4...",     // random UUID
  "nonce": "deadbeef01234567...",    // 32-byte hex
  "issued_at": 1740000000,           // unix timestamp (TTL: 30s)
  "valid_proof": { ... },            // ZKProof โ€” must verify as true
  "invalid_proof": { ... }           // ZKProof mutated with nonce โ€” must verify as false
}

// Response 200
{
  "challenge_id": "a3f1b2c4...",
  "result_valid": true,
  "result_invalid": false,
  "verified_at": 1740000002,
  "node_did": "did:key:z6Mk...",
  "signature": "ed25519:abc..."
}

// Response 400 โ€” missing fields or expired challenge
// Response 403 โ€” peer rejected (used in POST /peers/register flow)

POST /token/renew v0.3.6

Auto-renew an SPT that is near expiry (< 1h remaining) or recently expired (grace window: 7 days).

POST /token/renew
Content-Type: application/json

{ "spt": "<current_spt_string>" }

// Response 200
{
  "spt": "<new_spt_string>",
  "expires_in": 86400,
  "renewed": true,
  "method": "preemptive",   // or "grace_window"
  "old_expired": false,
  "node_did": "did:key:z6Mk..."
}

// Response 400 โ€” token still valid (> 1h remaining), includes renew_after
// Response 401 โ€” token expired > 7 days (full re-verification required)
// Response 403 โ€” DID not registered or score below floor
// Response 429 โ€” cooldown (60s between renewals per DID)

DPoP โ€” Demonstrating Proof of Possession v0.3.8

Prevents stolen SPT abuse. Every request must carry a fresh signed proof. Without the private key, a stolen token is useless.

Header: X-Soulprint-Proof: <base64url-proof>

// Proof payload (Ed25519 signed):
{
  "typ":      "soulprint-dpop",
  "method":   "POST",             // HTTP method โ€” must match request
  "url":      "https://...",      // exact URL โ€” must match request
  "nonce":    "a3f1b2...",        // 16 random bytes hex โ€” unique per request
  "iat":      1740000000,         // Unix timestamp โ€” expires in 300s
  "spt_hash": "sha256(spt)"       // bound to this specific token
}
// Serialized: base64url(JSON.stringify({ payload, signature, did }))

// Generate (client-side):
import { signDPoP, serializeDPoP } from "soulprint-core";
const proof  = signDPoP(privateKey, did, "POST", url, myToken);
const header = serializeDPoP(proof);

// Enable on server (soulprint-express):
app.use(soulprint({ minScore: 65, requireDPoP: true }));
// โ†’ 401 { error: "dpop_required" } if proof header is missing

// Enable on MCP (soulprint-mcp):
server.use(soulprint({ minScore: 65, requireDPoP: true }));

// Attacks blocked: token theft ยท replay ยท URL MITM ยท method MITM ยท
//   DID mismatch ยท spt_hash mismatch ยท expired proof ยท malformed proof

MCPRegistry โ€” On-Chain Verified MCPs v0.3.9

Public registry of verified MCP servers on Base Sepolia. Agents can verify a server is legitimate before trusting it.

Contract: 0x59EA3c8f60ecbAe22B4c323A8dDc2b0BCd9D3C2a (Base Sepolia)

GET /mcps/verified

List all currently verified MCPs. Public, no authentication required.

GET /mcps/verified

// Response 200
{
  "total": 1,
  "registry": {
    "contract":   "0x59EA3c8f60ecbAe22B4c323A8dDc2b0BCd9D3C2a",
    "network":    "Base Sepolia (chainId: 84532)",
    "superAdmin": "0x0755...",
    "totalMCPs":  1,
    "explorer":   "https://sepolia.basescan.org/address/0x59EA..."
  },
  "mcps": [
    {
      "address":     "0x...",
      "name":        "MCP Colombia Hub",
      "url":         "https://npmjs.com/package/mcp-colombia-hub",
      "category":    "general",
      "description": "...",
      "verified_at": "2026-02-25T01:02:24.000Z",
      "badge":       "โœ… VERIFIED"
    }
  ]
}

GET /mcps/all

List all registered MCPs (verified + pending). Public.

GET /mcps/status/:address

Check verification status of a specific MCP address.

GET /mcps/status/0x0755A3001F488da00088838c4a068dF7f883ad87

// Response 200
{
  "address":      "0x...",
  "name":         "MCP Colombia Hub",
  "registered":   true,
  "verified":     true,
  "registered_at": "2026-02-25T01:02:22.000Z",
  "verified_at":  "2026-02-25T01:02:24.000Z",
  "revoked_at":   null,
  "badge":        "โœ… VERIFIED by Soulprint"
}

// Response 404 โ€” not registered
{ "address": "0x...", "registered": false, "verified": false }

POST /admin/mcp/register (permissionless)

Register a new MCP on-chain. Anyone can register; verification requires admin approval.

POST /admin/mcp/register
Content-Type: application/json

{
  "ownerKey":    "0x<private_key>",   // signs the on-chain tx
  "address":     "0x<mcp_address>",
  "name":        "My Finance MCP",
  "url":         "https://my-mcp.example.com",
  "category":    "finance",            // finance|travel|jobs|ecommerce|general
  "description": "Provides Colombian financial data"
}

// Response 201
{
  "address":   "0x...",
  "name":      "My Finance MCP",
  "txHash":    "0x...",
  "explorer":  "https://sepolia.basescan.org/tx/0x...",
  "message":   "โœ… MCP registered. Contact Soulprint admin to verify.",
  "next_step": "POST /admin/mcp/verify with Bearer ADMIN_TOKEN"
}

POST /admin/mcp/verify ๐Ÿ” Admin only

Mark an MCP as verified on-chain. Requires Authorization: Bearer ADMIN_TOKEN + server-side ADMIN_PRIVATE_KEY.

POST /admin/mcp/verify
Authorization: Bearer <ADMIN_TOKEN>
Content-Type: application/json

{ "address": "0x<mcp_address>" }

// Response 200
{
  "address":  "0x...",
  "verified": true,
  "txHash":   "0x...",
  "explorer": "https://sepolia.basescan.org/tx/0x...",
  "message":  "โœ… MCP 0x... verified on-chain by Soulprint"
}

// Response 401 โ€” missing or wrong ADMIN_TOKEN
// Response 503 โ€” ADMIN_PRIVATE_KEY not configured on this node

POST /admin/mcp/revoke ๐Ÿ” Admin only

Revoke an MCP's verification. The reason is stored permanently on-chain.

POST /admin/mcp/revoke
Authorization: Bearer <ADMIN_TOKEN>
Content-Type: application/json

{ "address": "0x<mcp_address>", "reason": "Malicious behavior detected" }

// Response 200
{
  "address": "0x...",
  "revoked": true,
  "reason":  "Malicious behavior detected",
  "txHash":  "0x...",
  "message": "๐Ÿšซ MCP 0x... revoked. Reason: 'Malicious behavior detected'"
}

ProtocolThresholds โ€” On-Chain Governance v0.4.1

Protocol thresholds (SCORE_FLOOR, FACE_SIM_*, etc.) live on ProtocolThresholds.sol (Base Sepolia: 0xD8f78d65b35806101672A49801b57F743f2D2ab1). Only the superAdmin can modify them; anyone can read. Validators load them at startup and refresh every 10 minutes.

GET /protocol/thresholds

Returns all live protocol thresholds loaded from the blockchain.

GET /protocol/thresholds

Response 200

{
  "source":    "blockchain",
  "contract":  "0xD8f78d65b35806101672A49801b57F743f2D2ab1",
  "chain":     "Base Sepolia (chainId: 84532)",
  "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
  },
  "last_loaded": "2026-02-24T22:00:00.000Z",
  "note": "Solo el superAdmin del contrato puede modificar estos valores on-chain"
}

source: "blockchain" when loaded from chain, "local_fallback" if RPC is unreachable at node startup.

On-Chain Read (Solidity / ethers.js)

const ABI = [
  "function getThreshold(string calldata name) external view returns (uint256)",
  "function getAll() external view returns (string[] memory names, uint256[] memory values)",
];
const contract = new ethers.Contract(
  "0xD8f78d65b35806101672A49801b57F743f2D2ab1", ABI, provider
);
const scoreFloor = await contract.getThreshold("SCORE_FLOOR");  // โ†’ 65n
const [names, values] = await contract.getAll();                // โ†’ 9 entries

SuperAdmin โ€” Update Threshold

Only callable by the superAdmin wallet. Emits ThresholdUpdated(key, oldValue, newValue, by, timestamp).

// Only superAdmin
const ABI_WRITE = [
  "function setThreshold(string calldata name, uint256 value) external",
];
const c = new ethers.Contract(CONTRACT_ADDRESS, ABI_WRITE, adminWallet);
await (await c.setThreshold("SCORE_FLOOR", 70)).wait();

Canonical Values (v0.4.1)

NameValueNotes
SCORE_FLOOR65Min score for protected services
VERIFIED_SCORE_FLOOR52Min combined identity+reputation
MIN_ATTESTER_SCORE65Min score to issue attestations
FACE_SIM_DOC_SELFIE350 (0.35)Doc-photo vs selfie cosine similarity ร—1000
FACE_SIM_SELFIE_SELFIE650 (0.65)Selfie vs selfie ร—1000
DEFAULT_REPUTATION10Initial reputation for new identities
IDENTITY_MAX80Max identity contribution to score
REPUTATION_MAX20Max reputation contribution to score