Countersig Documentation
Countersig is the Trust Infrastructure for the Agent Era. Every AI agent gets a cryptographic identity, a 5-factor reputation score, and an embeddable trust badge.
Author: David Cooper, CCIE #14019 | Version: 1.0.0
What is Countersig?
Countersig bridges three systems that exist independently but for comprehensive agent identity: Ed25519 and SECP256K1 agent authentication, the SAID Identity Gateway, and a configurable reputation scoring engine.
Node.js / Express / PG
JSON / SVG / iframe
Core Primitives
Cryptographic Identity
Ed25519 PKI challenge-response verification. Industry-standard challenge-response protocol. No new trust assumptions.
5-Factor Reputation Score
Fee activity, success rate, age, SAID trust, and community attestations combine into a single 0–100 Reputation Score.
Embeddable Trust Badge
One iframe embed or JSON API call. Any application can display an agent's verification status instantly.
Agent Discovery
Public registry sorted by reputation score. Find verified agents by capability, status, or score.
Trust Score Model
| Factor | Weight | Max Points | How to Improve |
|---|---|---|---|
| ⚡ Fee Activity | 30% | 30 pts | More active on-chain usage (1 pt per 0.1 SOL) |
| ✅ Success Rate | 25% | 25 pts | Ensure your agent operates reliably |
| 📅 Registration Age | 20% | 20 pts | 1 point per day, max 20 (builds automatically) |
| 🌐 SAID Trust | 15% | 15 pts | Cross-chain reputation via SAID Protocol |
| 🤝 Community | 10% | 10 pts | Avoid being flagged (10=none, 5=one, 0=two+) |
Score Tiers
Agent Owner Guide
Everything you need to register your AI agent, get verified, and display a trust badge to users across supported chains.
Prerequisites
1. Ed25519 Keypair (Public Key + Private Key)
Think of it like a digital lock and key: your Public Key is the lock (share it freely), and your Private Key is the key (never share it — it proves ownership).
Generate Your Keypair
cd Countersig/backend
node -e "const nacl = require('tweetnacl'); const bs58 = require('bs58'); \
const kp = nacl.sign.keyPair(); \
console.log('PUBLIC KEY:', bs58.encode(kp.publicKey)); \
console.log('PRIVATE KEY:', bs58.encode(kp.secretKey));"
• NEVER share your private key with anyone — not even Countersig support
• Store your private key in a password manager, encrypted file, or secure env variable
• If you lose your private key, there is no recovery mechanism — you'll need to register a new agent
• Anyone with your private key can impersonate your agent
2. Your Agent's Basic Information
- Name — e.g. "TradeBot Pro", "CryptoAssistant"
- Description — 1–2 sentences about what your agent does
- Capabilities — comma-separated list (e.g. trading, analysis, notifications)
- X/Twitter Handle (optional) — for social verification
- Wallet Address (optional) — associated blockchain address
Step 1: Register Your Agent
You have three options. Choose the one that works best for you.
Go to countersig.com/register and follow the 4-step wizard:
550e8400-e29b-41d4-a716-446655440000). Save this — you'll need it for everything else!The demo page at countersig.com/demo is perfect for learning the flow before using your real keys. It will:
- Generate a temporary keypair in your browser
- Walk through all 4 steps automatically
- Show you exactly what happens at each stage
- Display the trust badge at the end
Use this to familiarize yourself, then use the Web Interface for your real registration.
curl -X POST https://api.countersig.com/register \
-H "Content-Type: application/json" \
-d '{
"pubkey": "YOUR_PUBLIC_KEY",
"name": "My Agent",
"description": "An AI agent that helps with trading",
"capabilities": ["trading", "analysis"],
"signature": "YOUR_SIGNATURE",
"message": "YOUR_MESSAGE",
"nonce": "YOUR_NONCE"
}'
The signature requires Ed25519 signing — see the Trustmark Developer Guide for complete code examples.
Step 2: Verify Your Agent (PKI Challenge-Response)
Verification proves you actually control the private key. This is what makes your agent Verified instead of just Registered.
| Status | Meaning |
|---|---|
| UNVERIFIED | Registered but key ownership not proven. Lower trust tier applies. |
| VERIFIED | Challenge-response complete, proving you control the private key. Full trust score applies. |
Verification via curl
# Step 1: Request a challenge
curl -X POST https://api.countersig.com/verify/challenge \
-H "Content-Type: application/json" \
-d '{"agentId": "YOUR_AGENT_ID"}'
# Response: {"nonce": "...", "challenge": "...", "expiresIn": 300}
# Step 2: Sign the challenge with your private key (see Developer Guide)
# Step 3: Submit verification
curl -X POST https://api.countersig.com/verify/response \
-H "Content-Type: application/json" \
-d '{
"agentId": "YOUR_AGENT_ID",
"nonce": "NONCE_FROM_STEP_1",
"signature": "YOUR_SIGNATURE"
}'
Step 3: Display Your Trust Mark
Option 1: Embeddable Widget (Recommended)
Auto-refreshes every 60 seconds. Shows name, trust score, verification status, capabilities, and links to full profile.
Option 2: SVG Badge (for README / Docs)

Option 3: JSON API (for Custom Displays)
GET https://api.countersig.com/badge/YOUR_AGENT_ID
{
"status": "success",
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Agent",
"badge": "VERIFIED",
"label": "Gold",
"tier": "gold",
"score": 75,
"capabilities": ["trading", "analysis"],
"verificationStatus": "verified",
"widgetUrl": "https://api.countersig.com/widget/550e8400..."
}
Understanding Your Trust Score
Your trust score is a number from 0–100. New agents start at approximately 10/100 — this is normal. Score grows as your agent accumulates activity and age.
| Factor | Weight | Description | How to Improve |
|---|---|---|---|
| ⚡ Fee Activity | 30 pts | On-chain fee volume on Bags.fm | More active on-chain usage |
| ✅ Success Rate | 25 pts | Successful actions / total actions | Ensure reliable operation |
| 📅 Age | 20 pts | Days since registration | 1 pt/day — builds automatically |
| 🌐 SAID Trust | 15 pts | Cross-chain trust via SAID Protocol | Build cross-chain reputation |
| 🤝 Community | 10 pts | Flag penalty | Avoid flags: 10=none, 5=one, 0=two+ |
Common Pitfalls to Avoid
bs58.decode(message) to workVerification Checklist
Widget Integration Guide
The Countersig Widget provides a visual trust indicator that can be embedded in any web application, documentation, or marketplace listing, displaying real-time reputation data.
Quick Start
iframe Embed (Recommended)
Customization
Size Variations
| Use Case | Width | Height | Notes |
|---|---|---|---|
| Compact | 320px | 200px | Sidebar placement, minimal display |
| Standard | 400px | 300px | Recommended for most use cases |
| Full | 100% | 400px | Dashboard or profile pages |
URL Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
theme | string | auto | Color theme: dark, light, or auto |
compact | boolean | false | Compact mode — smaller fonts, tighter spacing |
CSS Styling
.widget-container {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
transition: transform 0.2s ease;
}
.widget-container:hover {
transform: translateY(-2px);
}
.widget-container iframe {
display: block;
width: 100%;
border: none;
}
Self-Hosted Widget (React)
const statusConfig = {
verified: { icon: '✅', label: 'VERIFIED AGENT', accentColor: '#22c55e' },
unverified: { icon: '⚠️', label: 'UNVERIFIED', accentColor: '#f59e0b' },
flagged: { icon: '🔴', label: 'FLAGGED', accentColor: '#ef4444' }
};
import { useState, useEffect } from 'react';
function CountersigWidget({ agentId }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(`https://api.countersig.com/badge/${agentId}`)
.then(res => res.json())
.then(setData);
// Auto-refresh every 60 seconds
const interval = setInterval(() => {
fetch(`https://api.countersig.com/badge/${agentId}`)
.then(res => res.json())
.then(setData);
}, 60000);
return () => clearInterval(interval);
}, [agentId]);
if (!data) return Loading...;
const config = statusConfig[data.status];
return (
{config.icon}
{config.label}
{data.name}
{data.score}
TRUST SCORE
);
}
Badge Data Response
{
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Trading Agent",
"status": "verified",
"score": 75,
"bags_score": 75,
"saidTrustScore": 85,
"saidLabel": "HIGH",
"registeredAt": "2024-01-15T10:30:00.000Z",
"lastVerified": "2024-01-20T14:22:00.000Z",
"totalActions": 150,
"successRate": 0.94,
"capabilities": ["trading", "analytics"],
"widgetUrl": "https://api.countersig.com/widget/AgentPubkey..."
}
SVG Badge for README / Docs

[](https://countersig.com/agents/{AGENT_ID})
Troubleshooting
CORS Errors — Access-Control-Allow-Origin header missing
Set the CORS_ORIGIN env variable to your domain. For multiple origins, use comma-separated values. For dev only, * works.
CORS_ORIGIN=https://yourapp.com,https://anotherapp.com
Widget Not Loading — iframe shows blank or error
Verify the agent ID is correct, check for 404 errors in console, confirm the agent is registered:
curl https://api.countersig.com/badge/{AGENT_ID}
Data Not Updating — badge shows stale data
The widget auto-refreshes every 60 seconds. Badge data is cached for 60 seconds (configurable via BADGE_CACHE_TTL). Force refresh with a cache-buster param: ?t=123456.
Rate Limiting — 429 Too Many Requests
Default: 100 requests per 15 minutes per IP. Implement client-side caching. For high-traffic apps, contact support for increased limits.
Best Practices
function isValidPubkey(pubkey) {
return /^[A-HJ-NP-Za-km-z1-9]{32,44}$/.test(pubkey);
}
Trustmark Developer Integration Guide
Technical implementation details and code examples for developers integrating Countersig trust verification into their applications.
Technical Requirements
| Language | Library | Notes |
|---|---|---|
| Node.js 18+ | tweetnacl, bs58 | Primary/recommended |
| Python | pynacl | PyNaCl |
| Go | crypto/ed25519 | Standard library |
| Rust | ed25519-dalek | Crates.io |
| Java | net.i2p.crypto:eddsa | Maven Central |
Quick Start
npm install tweetnacl bs58
Keypair Generation
const nacl = require('tweetnacl');
const bs58 = require('bs58');
// Generate a new Ed25519 keypair
const keypair = nacl.sign.keyPair();
// Encode keys as base58 strings (standard for Solana ecosystem)
const pubkey = bs58.encode(keypair.publicKey); // 32 bytes → ~44 chars
const secretKey = bs58.encode(keypair.secretKey); // 64 bytes → ~88 chars
console.log('PUBLIC KEY:', pubkey);
console.log('SECRET KEY:', secretKey); // STORE SECURELY — NEVER COMMIT
// Restore keypair from stored secret key
const storedSecretKey = process.env.AGENT_SECRET_KEY;
const restoredKeypair = nacl.sign.keyPair.fromSecretKey(
bs58.decode(storedSecretKey)
);
Signing a Challenge
const nacl = require('tweetnacl');
const bs58 = require('bs58');
// Challenge message format: AGENTID-VERIFY:{agentId}:{pubkey}:{nonce}:{timestamp}
// Note: Protocol message prefixes (AGENTID-VERIFY, AGENTID-REGISTER, etc.) retain
// their original format for backward compatibility with existing Ed25519 signatures.
function signChallenge(challengeMessage, secretKeyBase58) {
const secretKeyBytes = bs58.decode(secretKeyBase58);
const keypair = nacl.sign.keyPair.fromSecretKey(secretKeyBytes);
// The challenge from the API is base58-encoded — decode it
const messageBytes = bs58.decode(challengeMessage);
// Sign using Ed25519
const signatureBytes = nacl.sign.detached(messageBytes, keypair.secretKey);
// Return base58-encoded signature
return bs58.encode(signatureBytes);
}
// Usage
const signature = signChallenge(challenge, process.env.AGENT_SECRET_KEY);
Full Registration Flow
const nacl = require('tweetnacl');
const bs58 = require('bs58');
async function registerAgent(config) {
const { pubkey, secretKey, name, description, capabilities } = config;
const API_BASE = 'https://api.countersig.com';
// Step 1: Register the agent first to get an agentId
// (Registration requires a pre-signed challenge - see PKI Challenge flow)
// After registration, you'll receive an agentId
// Step 2: Request a verification challenge using agentId
const challengeRes = await fetch(`${API_BASE}/verify/challenge`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agentId: 'YOUR_AGENT_ID' })
});
const { nonce, challenge } = await challengeRes.json();
// Step 3: Sign the challenge
const secretKeyBytes = bs58.decode(secretKey);
const keypair = nacl.sign.keyPair.fromSecretKey(secretKeyBytes);
const messageBytes = bs58.decode(challenge);
const signatureBytes = nacl.sign.detached(messageBytes, keypair.secretKey);
const signature = bs58.encode(signatureBytes);
// Step 4: Submit verification response
const verifyRes = await fetch(`${API_BASE}/verify/response`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'YOUR_AGENT_ID',
nonce,
signature
})
});
const result = await verifyRes.json();
if (!verifyRes.ok) throw new Error(result.error);
console.log('Agent verified! ID:', result.agentId);
return result;
}
// Execute verification flow (registration is a separate step)
registerAgent({
pubkey: process.env.AGENT_PUBKEY,
secretKey: process.env.AGENT_SECRET_KEY,
name: 'My Trading Agent',
description: 'Automated DeFi trading agent',
capabilities: ['trading', 'analytics']
});
// Note: The registration endpoint (/register) also requires a signature.
// The flow is: 1) Generate keypair → 2) Sign registration message → 3) Register → 4) Verify via challenge-response
Other Languages
import nacl.signing
import base58
# Generate keypair
signing_key = nacl.signing.SigningKey.generate()
verify_key = signing_key.verify_key
pubkey = base58.b58encode(bytes(verify_key)).decode()
secret = base58.b58encode(bytes(signing_key)).decode()
# Sign a challenge
def sign_challenge(challenge_b58, secret_key_b58):
secret_bytes = base58.b58decode(secret_key_b58)
signing_key = nacl.signing.SigningKey(secret_bytes)
message_bytes = base58.b58decode(challenge_b58)
signed = signing_key.sign(message_bytes)
return base58.b58encode(signed.signature).decode()
package main
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
)
func generateKeypair() (ed25519.PublicKey, ed25519.PrivateKey) {
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
return pub, priv
}
func signChallenge(challenge []byte, privateKey ed25519.PrivateKey) string {
signature := ed25519.Sign(privateKey, challenge)
return base64.StdEncoding.EncodeToString(signature)
}
func main() {
pub, priv := generateKeypair()
fmt.Println("Public Key:", base64.StdEncoding.EncodeToString(pub))
_ = priv // Store securely
}
use ed25519_dalek::{Keypair, Signer};
use rand::rngs::OsRng;
use bs58;
fn main() {
// Generate keypair
let mut csprng = OsRng{};
let keypair: Keypair = Keypair::generate(&mut csprng);
let pubkey = bs58::encode(keypair.public.as_bytes()).into_string();
let secret = bs58::encode(keypair.secret.as_bytes()).into_string();
println!("PUBLIC KEY: {}", pubkey);
// Sign a challenge
let message = b"AGENTID-VERIFY:...";
let signature = keypair.sign(message);
let sig_b58 = bs58::encode(signature.as_ref()).into_string();
println!("SIGNATURE: {}", sig_b58);
}
API Reference
Complete reference for all Countersig API endpoints. Base URL: https://api.countersig.com
Authentication Model
Countersig uses an Ed25519 signature-based authentication model for all state-modifying operations — no API keys, no shared secrets. Challenge messages follow the exact format:
AGENTID-VERIFY:{agentId}:{pubkey}:{nonce}:{timestamp}
Note: Protocol message prefixes (AGENTID-VERIFY, AGENTID-REGISTER, etc.) retain their original format for backward compatibility with existing Ed25519 signatures.
POST /verify/challengeconst message = `AGENTID-VERIFY:${agentId}:${pubkey}:${nonce}:${timestamp}`;
const messageBytes = Buffer.from(message, 'utf-8');
const signatureBytes = nacl.sign.detached(messageBytes, privateKeyBytes);
const signature = bs58.encode(signatureBytes);
Rate Limiting
| Tier | Endpoints | Limit | Window |
|---|---|---|---|
| Default | Read operations (GET) | 100 requests | 15 minutes |
| Auth | Write operations (POST/PUT), auth | 20 requests | 15 minutes |
Error Handling
| Code | Meaning | Common Causes |
|---|---|---|
200 | OK | Request successful |
201 | Created | Resource created |
400 | Bad Request | Missing/invalid params, validation failure |
401 | Unauthorized | Invalid signature, expired challenge, stale timestamp |
404 | Not Found | Agent or challenge not found |
409 | Conflict | Agent already registered |
429 | Too Many Requests | Rate limit exceeded |
500 | Server Error | Server-side error |
Reputation Scoring
// Fee Activity: min(30, floor(totalFeesSOL * 10))
// Success Rate: floor((successful / total) * 25)
// Age: min(20, daysSinceRegistration)
// SAID Trust: floor((saidScore / 100) * 15)
// Community: 10 if 0 flags, 5 if 1 flag, 0 if 2+ flags
const totalScore = feeActivity + successRate + age + saidTrust + community;
Agents with 3 or more unresolved flags are automatically marked status: 'flagged' regardless of score.
Endpoints
Request Body
| Field | Type | Required | Constraints |
|---|---|---|---|
pubkey | string | ✅ | Valid Solana address (32–88 chars) |
name | string | ✅ | 1–255 characters |
signature | string | ✅ | Base58-encoded Ed25519 signature |
message | string | ✅ | Must contain the nonce |
nonce | string | ✅ | Challenge nonce |
tokenMint | string | — | Token mint address for fee tracking |
capabilities | string[] | — | Array of capability strings |
description | string | — | Agent description |
creatorX | string | — | X/Twitter handle |
creatorWallet | string | — | Creator's wallet address |
curl -X POST https://api.countersig.com/register \
-H "Content-Type: application/json" \
-d '{
"pubkey": "AgentPubkey111...",
"name": "My Trading Agent",
"signature": "Base58Sig...",
"message": "AGENTID-VERIFY:...",
"nonce": "550e8400-e29b-41d4-a716-446655440000",
"capabilities": ["trading", "analytics"]
}'
Signature message format: AGENTID-UPDATE:{agentId}:{timestamp}
Note: Protocol message prefixes (AGENTID-UPDATE, AGENTID-VERIFY, etc.) retain their original format for backward compatibility.
curl -X PUT https://api.countersig.com/agents/{AGENT_ID}/update \
-H "Content-Type: application/json" \
-d '{
"signature": "Base58Sig...",
"timestamp": 1705753200000,
"name": "Updated Agent Name",
"capabilities": ["trading", "analytics", "reporting"]
}'
curl -X POST https://api.countersig.com/verify/challenge \
-H "Content-Type: application/json" \
-d '{"agentId": "550e8400-e29b-41d4-a716-446655440000"}'
// Response 200 OK
{
"nonce": "550e8400-e29b-41d4-a716-446655440000",
"challenge": "8n3kQm9XvB2pL5rT...",
"expiresIn": 300
}
curl -X POST https://api.countersig.com/verify/response \
-H "Content-Type: application/json" \
-d '{
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"nonce": "550e8400-...",
"signature": "Base58Sig..."
}'
Query params: ?page=1&limit=20&status=verified&capability=trading
// Response 200 OK
{
"pubkey": "OwnerPubkey111...",
"agents": [...],
"count": 3
}
{ "status": "ok", "timestamp": "2026-04-16T10:00:00.000Z" }
Developer Guide
Enterprise-grade developer onboarding for the Countersig platform — local setup, architecture, testing, and contributing.
Prerequisites
| Component | Min Version | Purpose |
|---|---|---|
| Node.js | 20.x | Runtime environment |
| PostgreSQL | 16.x | Primary database |
| Redis | 7.x | Caching and session storage |
| Git | 2.x+ | Version control |
Quick Start
# 1. Clone the repository
git clone https://github.com/RunTimeAdmin/Countersig.git
cd countersig
# 2. Start PostgreSQL + Redis with Docker Compose
docker-compose up -d
# 3. Install backend dependencies
cd backend && npm install
# 4. Copy and configure environment
cp .env.example .env
# Edit .env with your settings
# 5. Run database migrations
npm run migrate
# 6. Start the backend
npm start
# Backend runs on http://localhost:3002
# 7. In a new terminal — install + start frontend
cd ../frontend && npm install && npm run dev
# Frontend runs on http://localhost:5174
Environment Configuration
# Database
DATABASE_URL=postgresql://countersig:password@localhost:5432/countersig
# Redis
REDIS_URL=redis://localhost:6379
# Bags.fm API
BAGS_API_KEY=your_bags_api_key_here
# Server
PORT=3002
NODE_ENV=development
# CORS
CORS_ORIGIN=http://localhost:5174
# Identity
COUNTERSIG_BASE_URL=http://localhost:3002 # Production: https://api.countersig.com
# SAID Gateway
SAID_GATEWAY_URL=https://said-gateway.example.com
# Cache TTL (seconds)
BADGE_CACHE_TTL=60
CHALLENGE_EXPIRY_SECONDS=300
Database Setup
-- agent_identities: core registration table
CREATE TABLE agent_identities (
agent_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pubkey VARCHAR(88) NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
token_mint VARCHAR(88),
capability_set JSONB DEFAULT '[]',
creator_x VARCHAR(255),
creator_wallet VARCHAR(88),
status VARCHAR(20) DEFAULT 'active',
bags_score INTEGER DEFAULT 0,
total_actions INTEGER DEFAULT 0,
successful_actions INTEGER DEFAULT 0,
failed_actions INTEGER DEFAULT 0,
registered_at TIMESTAMPTZ DEFAULT NOW(),
last_verified TIMESTAMPTZ,
is_demo BOOLEAN DEFAULT false,
UNIQUE(pubkey, name)
);
CREATE INDEX idx_agent_identities_pubkey ON agent_identities(pubkey);
-- agent_verifications: PKI challenge tracking
CREATE TABLE agent_verifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id UUID REFERENCES agent_identities(agent_id),
pubkey VARCHAR(88),
nonce VARCHAR(255) UNIQUE NOT NULL,
challenge TEXT NOT NULL,
completed BOOLEAN DEFAULT false,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_agent_verifications_agent_id ON agent_verifications(agent_id);
-- agent_flags: community reporting
CREATE TABLE agent_flags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id UUID REFERENCES agent_identities(agent_id),
pubkey VARCHAR(88),
reporter_pubkey VARCHAR(88),
reason TEXT NOT NULL,
resolved BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_agent_flags_agent_id ON agent_flags(agent_id);
Running the Project
# Development (with hot reload)
npm run dev
# Production
npm start
# Run tests (34 tests, Jest)
npm test
# Watch mode
npm run test:watch
Architecture Overview
Backend: Node.js 20 + Express
├── routes/ (7 route modules)
├── services/ (5 services: bagsAuth, bagsRep, badgeBuilder, pkiChallenge, saidBinding)
├── middleware/ (auth, rate limiting)
├── models/ (PostgreSQL queries)
└── utils/ (transform, validation)
Frontend: React 18 + Vite + Tailwind CSS v4
├── pages/ (Registry, AgentDetail, Register, Discover)
├── components/ (AgentCard, FlagModal, TrustBadge, Pagination)
└── widget/ (standalone embeddable iframe bundle)
Crypto: tweetnacl (Ed25519 sign/verify) + bs58 (encoding)
Tests: Jest, 34 passing tests (pkiChallenge, bagsReputation, transform)
Testing
# Run all 34 tests
npm test
# Test files
backend/tests/
├── pkiChallenge.test.js # Ed25519 keypair challenge/verify
├── bagsReputation.test.js # 5-factor scoring, label thresholds
└── transform.test.js # snakeToCamel, escapeHtml, isValidSolanaAddress
Deployment Guide
Complete guide for deploying Countersig on Ubuntu 22.04 VPS with PM2, Nginx, PostgreSQL, and Redis.
PostgreSQL 14 | Redis | Node.js 20 | PM2 | Nginx (SSL)
Pre-Flight Check
# Verify PostgreSQL
sudo systemctl status postgresql
# Verify Redis
sudo systemctl status redis-server
# Verify Node.js
node --version # Should be v20.x.x
# Verify PM2
pm2 --version
# Check port 3002 is free
sudo lsof -i :3002
Clone & Install
cd /var/www
git clone https://github.com/RunTimeAdmin/Countersig.git
cd countersig/backend
npm install --production
Environment Setup
cp .env.example .env
nano .env
DATABASE_URL=postgresql://countersig:STRONGPASSWORD@localhost:5432/countersig
REDIS_URL=redis://localhost:6379
BAGS_API_KEY=your_production_bags_api_key
PORT=3002
NODE_ENV=production
CORS_ORIGIN=https://countersig.com
COUNTERSIG_BASE_URL=https://api.countersig.com
Database Setup
# Create database and user
sudo -u postgres psql -c "CREATE USER countersig WITH PASSWORD 'STRONGPASSWORD';"
sudo -u postgres psql -c "CREATE DATABASE countersig OWNER countersig;"
# Run migrations
cd /var/www/countersig/backend
npm run migrate
PM2 Configuration
# Start Countersig backend
pm2 start server.js --name countersig --cwd /var/www/countersig/backend
# Save PM2 config (survives reboots)
pm2 save
pm2 startup # Follow the printed command
# Check status
pm2 status
pm2 logs countersig
Nginx Configuration
server {
listen 80;
server_name countersig.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name countersig.com;
ssl_certificate /etc/letsencrypt/live/countersig.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/countersig.com/privkey.pem;
# API backend - routes mounted at root (no /api/ prefix)
location ~ ^/(register|verify|agents|badge|widget|health|discover|reputation) {
proxy_pass http://localhost:3002;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Frontend static files
location / {
root /var/www/countersig/frontend/dist;
try_files $uri $uri/ /index.html;
}
}
# Test and reload nginx
sudo nginx -t && sudo systemctl reload nginx
Verify Deployment
# Health check
curl https://api.countersig.com/health
# List agents
curl https://api.countersig.com/agents
# PM2 process list
pm2 list