VINAC-FM Integration Guide
VINAC-FM (Verified Inaudible Nonce Acoustic Carrier — Frequency Modulated) provides Layer 4 physical presence verification using ultrasonic signals. This guide covers every step from requesting a challenge to resolving a signed certificate on your server, including replay attack prevention.
Ultrasonic
18–22 kHz, inaudible carrier
Signed Cert
ECDSA P-256 / ES256
Single-use
Atomic nonce & cert burn
< 5 sec
Full round-trip latency
How it works
Request challenge
Client calls vinacfm-challenge. Server creates a vinac_session with a random nonce embedded in an ultrasonic signal plan.
Emit signal
VinacAudioEngine plays the nonce-encoded tone through the device speaker at the challenge frequency (e.g. 18 kHz).
Capture via mic
The same device (or a co-located receiver) captures the signal and extracts the nonce from the PCM buffer.
Submit proof
Client sends the captured nonce + signal metadata to vinacfm-verify. The server atomically burns the nonce — one-time use, race-free.
Receive certificate
On success the server issues an ECDSA-signed vinac_certificate with a 24-hour TTL. The token is single-use.
Resolve on backend
Your server calls vinacfm-resolve with the certificate token. The server atomically marks it consumed and returns the action payload.
Prerequisites
Initialize the client
Install the SDK and configure your project credentials. All sensitive keys stay server-side; only the publicKey is exposed to the browser.
import { SilentAuth } from '@silentauth/sdk';const sa = new SilentAuth({projectId: process.env.SA_PROJECT_ID,secretKey: process.env.SA_SECRET_KEY,});
Request a challenge
Call requestChallenge() to obtain a fresh acoustic challenge. The returned jti uniquely identifies this challenge issuance — it is bound to a single vinac_session and cannot be reused.
// Browser / React — request an acoustic challengeimport { requestChallenge } from '@silentauth/vinacfm';const challenge = await requestChallenge(projectId, // your project UUIDpublicKey, // your project public key);// challenge.vinac_session_id — bind this to your verify session// challenge.challenge_nonce — encoded into the ultrasonic signal// challenge.frequency_hz — carrier frequency (e.g. 18000)// challenge.duration_ms — transmission window (e.g. 3000)// challenge.expires_at — ISO timestamp, ~30 seconds
Challenges expire in ~30 seconds. Request one only when the user is ready to verify. Each challenge is single-use — once a proof is submitted (success or failure), the nonce is permanently burned.
Emit the ultrasonic signal
VinacAudioEngine.runFull() plays the nonce-encoded tone through the Web Audio API and simultaneously records via the microphone. In environments without microphone access, use runEmitOnly() — the server still validates the signal hash.
import { VinacAudioEngine } from '@silentauth/vinacfm';const engine = new VinacAudioEngine({onProgress: (pct) => setProgress(pct),onState: (s) => setStatus(s),});// Emit the nonce and optionally capture it via micconst result = await engine.runFull(challenge);// result.capturedNonce — the nonce received back via microphone// result.signalHash — SHA-256 of raw PCM buffer// result.dominantFrequency — peak frequency detected// result.snr — signal-to-noise ratio (dB)
Submit the proof
submitProof() sends the captured nonce to the server. The server atomically burns the nonce via UPDATE WHERE nonce_burned_at IS NULL — any concurrent or replayed submission races the same row and loses, receiving NONCE_REPLAYED.
import { submitProof } from '@silentauth/vinacfm';const verification = await submitProof(challenge.vinac_session_id,result.capturedNonce,publicKey,{signalHash: result.signalHash,deviceFingerprint: getDeviceFingerprint(),dominantFrequencyHz: result.dominantFrequency,snrDb: result.snr,// Replay prevention — always includerequest_timestamp: Date.now(),},);if (verification.verified) {const cert = verification.certificate;// cert.token — present this to your backend// cert.signed — true if ECDSA-signed// cert.signature — ES256 / P-256 signature}
Always include request_timestamp: Date.now(). The server rejects requests where the clock skew exceeds 5 minutes, preventing token-lift replay attacks from captured network traffic.
Resolve on your server
Pass the certificate token from your client to your backend and call resolveToken(). This is the final authority check — the server atomically marks the certificate consumed (UPDATE WHERE consumed_at IS NULL). Only the first successful resolution proceeds; all subsequent calls for the same token receive CERT_REPLAYED.
// Server-side (Node.js / any backend)import { resolveToken } from '@silentauth/vinacfm/server';const result = await resolveToken(req.body.certificate_token,req.body.token_id,process.env.SA_PUBLIC_KEY,{ request_timestamp: Date.now() },);if (!result.resolved) {// result.error includes:// 'Certificate has already been consumed' (CERT_REPLAYED)// 'Certificate has expired' (CERT_EXPIRED)// 'Certificate has been revoked'return res.status(401).json({ error: result.error });}// Allow the action described by result.actionconsole.log(result.action.namespace); // e.g. "finance.transfer"console.log(result.action.risk_tier); // "high"console.log(result.action.execution_policy);
Drop-in React component
For React applications, VinacFmEmitter wraps the entire challenge–emit–capture–verify flow in a self-contained component. Pass onVerified to receive the signed certificate.
import { VinacFmEmitter } from '@silentauth/vinacfm/react';export function PaymentConfirmation() {return (<VinacFmEmitterprojectId={process.env.NEXT_PUBLIC_SA_PROJECT_ID}publicKey={process.env.NEXT_PUBLIC_SA_PUBLIC_KEY}onVerified={(cert) => {// cert.token is single-use — send to your backend immediatelyfetch('/api/payment/authorize', {method: 'POST',body: JSON.stringify({ vinac_cert: cert.token }),});}}onFailed={(err) => console.error('VINAC failed:', err)}/>);}
Props
projectIdstringYour SilentAuth project UUID
publicKeystringProject public key
verifySessionIdstring?Link to an existing verify session
onVerified(cert: VinacCertificate) => voidCalled with the issued certificate
onFailed(err: string) => voidCalled on verification failure
demobooleanRun without backend (demo mode)
Backend handler example
Your API route receives the certificate token from the client and resolves it server-side before authorizing the action.
// POST /api/payment/authorizeexport async function POST(req: Request) {const { vinac_cert, token_id } = await req.json();const result = await resolveToken(vinac_cert,token_id,process.env.SA_PUBLIC_KEY,);if (!result.resolved) {return Response.json({ error: result.error }, { status: 401 });}// result.action.requires_vinac is true — physical presence confirmedawait processPayment(/* ... */);return Response.json({ ok: true });}
Replay attack prevention
VINAC-FM v1.0 enforces replay prevention at every layer of the protocol. All defenses are server-enforced — no client co-operation required.
Atomic nonce burn
The verification UPDATE uses WHERE nonce_burned_at IS NULL AND status IN ('pending','active'). Only one concurrent request can win the race; all others receive NONCE_REPLAYED (HTTP 409).
Single-use certificates
resolve() atomically sets consumed_at via UPDATE WHERE consumed_at IS NULL. Any subsequent resolution — including concurrent requests — receives CERT_REPLAYED.
Clock skew enforcement
Requests with request_timestamp more than 5 minutes from server time are rejected with CLOCK_SKEW (HTTP 400). Prevents token-lift replay from captured network traffic.
Session terminal states
Once a session reaches completed, failed, or expired, all further submissions are rejected with SESSION_REPLAY (HTTP 409), even with a valid nonce.
Replay audit log
Every replay attempt is written to vinac_replay_log with the replay_type, project ID, reference ID, IP, and user agent. Review these in the Threat Intelligence dashboard.
Error codes
| Code | HTTP | Description |
|---|---|---|
| NONCE_REPLAYED | 409 | The acoustic challenge nonce was already consumed. Start a new challenge. |
| SESSION_REPLAY | 409 | The VINAC session is in a terminal state (completed/failed/expired). |
| SESSION_EXPIRED | 410 | The session TTL (~5 min) elapsed before proof was submitted. |
| CLOCK_SKEW | 400 | request_timestamp deviates more than 5 minutes from server time. |
| NONCE_MISMATCH | 200 | Acoustic proof received but nonce doesn't match the emitted challenge. |
| CERT_REPLAYED | 200 | Certificate token has already been resolved (single-use). |
| CERT_EXPIRED | 200 | The 24-hour certificate TTL has elapsed. |
Live demo
The widget below runs in demo mode — no backend required, no microphone permission needed. It plays the ultrasonic tone and simulates the full verification flow so you can preview the UX before integrating.
Requires speaker access · Microphone optional for loop-back verification
What you're seeing
Run with a real backend
Pass your project credentials to enable full verification:
<VinacFmEmitter
projectId="your-project-id"
publicKey="your-public-key"
onVerified={(cert) => console.log(cert)}
/>