This is a practical guide — no theory, just code. By the end you'll have a working verification flow: the user opens your page, proves they're human, and your server receives a confirmed `human: true` signal with a `trust_band` and anonymous `did_hash`. Total integration time: 20–30 minutes.
Prerequisites
- An APTOGON account and API keys (get them at /console)
- A web application — any framework works; examples use React and vanilla JS
- A server-side component — examples in Node.js and Python
Step 1: Get Your Keys
In the APTOGON console, create an API key pair. You'll receive two keys:
- `pk_live_…` — the publishable key. Safe to include in browser code.
- `sk_live_…` — the secret key. Server-side only. Shown once at creation — save it immediately.
Also register your domain in the console. Verification requests from unregistered origins are rejected.
Step 2: Add the Widget (Browser)
The fastest integration is the drop-in script. Add one script tag and one div:
html
<!-- Add before </body> -->
<script src="https://homosapience.org/embed/v1/aptogon.js"
data-aptogon-key="pk_live_YOUR_KEY"></script>
<!-- Place where you want the button -->
<div
data-aptogon-verify
data-on-success="handleVerified"
></div>
<script>
// Called when user completes verification
function handleVerified({ token, trust_band, did_hash }) {
// Send the token to your server for confirmation
fetch('/api/confirm-human', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
}).then(r => r.json()).then(result => {
if (result.human) {
// Unlock the protected action
document.getElementById('protected-content').style.display = 'block'
}
})
}
</script>
Step 2 (Alternative): React Component
tsx
import { useEffect, useRef } from 'react'
declare global {
interface Window { Aptogon: { verify: (opts: object) => Promise<VerifyResult> } }
}
type VerifyResult = {
token: string
trust_band: 'newcomer' | 'community' | 'trusted'
did_hash: string
}
export function HumanVerifyButton({ onVerified }: { onVerified: (r: VerifyResult) => void }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const script = document.createElement('script')
script.src = 'https://homosapience.org/embed/v1/aptogon.js'
script.setAttribute('data-aptogon-key', process.env.NEXT_PUBLIC_APTOGON_KEY!)
document.head.appendChild(script)
return () => { document.head.removeChild(script) }
}, [])
const handleClick = async () => {
const result = await window.Aptogon.verify({
publishableKey: process.env.NEXT_PUBLIC_APTOGON_KEY!,
})
onVerified(result)
}
return (
<button ref={ref} onClick={handleClick}
style={{ padding: '12px 28px', background: '#7c3aed', color: '#fff',
borderRadius: 10, fontWeight: 700, border: 'none', cursor: 'pointer' }}>
✍️ Verify I'm human
</button>
)
}
Step 3: Confirm Server-Side (Node.js)
The token the browser receives is short-lived (60 seconds) and must be confirmed server-side with your secret key. Never skip this step — client-side signals can be forged.
javascript
// pages/api/confirm-human.js (Next.js) or Express route
export default async function handler(req, res) {
const { token } = req.body
if (!token) return res.status(400).json({ error: 'token required' })
const response = await fetch('https://homosapience.org/api/embed/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.APTOGON_SECRET_KEY}`,
},
body: JSON.stringify({ token }),
})
if (!response.ok) {
return res.status(400).json({ human: false, error: 'verification failed' })
}
const { human, did_hash, trust_band } = await response.json()
if (human) {
// Optional: store did_hash to enforce one-action-per-human
// await db.recordHumanAction(did_hash, 'survey_response')
}
return res.json({ human, trust_band, did_hash })
}
Step 3 (Alternative): Python / FastAPI
python
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import os
app = FastAPI()
APTOGON_SECRET = os.environ["APTOGON_SECRET_KEY"]
class VerifyRequest(BaseModel):
token: str
@app.post("/api/confirm-human")
async def confirm_human(body: VerifyRequest):
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://homosapience.org/api/embed/verify",
headers={
"Authorization": f"Bearer {APTOGON_SECRET}",
"Content-Type": "application/json",
},
json={"token": body.token},
)
if resp.status_code != 200:
raise HTTPException(status_code=400, detail="verification failed")
data = resp.json()
# data = { "human": True, "did_hash": "sha3...", "trust_band": "community" }
return data
Step 4: Using did_hash for One-Human-One-Action
The `did_hash` is an anonymous fingerprint — a one-way hash with no link to real identity. Store it alongside actions to enforce uniqueness across sessions:
javascript
// Enforce one survey response per human
const existing = await db.query(
'SELECT id FROM survey_responses WHERE did_hash = $1 AND survey_id = $2',
[did_hash, surveyId]
)
if (existing.rows.length > 0) {
return res.status(409).json({ error: 'already_submitted' })
}
await db.query(
'INSERT INTO survey_responses (did_hash, survey_id, data) VALUES ($1, $2, $3)',
[did_hash, surveyId, responseData]
)
Step 5: Trust Band Gating (Optional)
Different actions can require different trust levels. Use `trust_band` to gate high-stakes actions:
javascript
// Require 'trusted' or 'community' for governance votes
// Allow 'newcomer' for forum posting
const TRUST_REQUIREMENTS = {
'/vote': ['trusted', 'community'],
'/post': ['trusted', 'community', 'newcomer'],
'/airdrop': ['trusted'],
}
const allowed = TRUST_REQUIREMENTS[action] ?? ['trusted', 'community', 'newcomer']
if (!allowed.includes(trust_band)) {
return res.status(403).json({
error: 'trust_band_insufficient',
required: allowed,
current: trust_band,
upgrade_hint: 'Complete more verifications and connect with verified humans to build your bond graph.',
})
}
Testing in Development
Use `pk_test_…` and `sk_test_…` keys from the console for local development. Test keys return synthetic verification results without going on-chain. The verification flow works identically — same API shape, same trust bands — but `human` is always `true` for any completed gesture.
In production, always use live keys. Test keys accept any gesture including programmatically submitted ones — they are intentionally insecure for testing purposes.
Common Pitfalls
- **Secret key in browser code**: if you see `sk_live_` in client-side JavaScript, you have a critical security issue. The secret key must only exist on your server.
- **Skipping server-side verification**: the token returned by the widget must be verified server-side. A token alone does not prove verification — it must be redeemed with the secret key.
- **Caching did_hash incorrectly**: `did_hash` is stable across sessions for the same device but can change if the user resets their device or browser. Do not use it as a permanent user identifier.
- **Not registering your domain**: verification requests from unregistered origins return 403. Add all your domains (including `localhost` for development) in the console.
That's the full integration. The verification flow adds roughly 10 seconds to the user experience — the gesture challenge opens in a popup and closes automatically on success. Your application receives a cryptographic proof of humanity, an anonymous identity hash, and a trust band, with zero personal data stored anywhere in the chain.