Open-source client v1.0 · Apache 2.0 · audited 03.26 by Trail of Bits
Streaming OPRF protocol · v3.1 methodology
Cryptographic privacy · Apache 2.0 · Audited

Audit any audience without sharing it.

VRA's open-source audit client uses an oblivious PRF protocol so your IPs never leave your machine in cleartext. We never see them. You get back grades. Production-ready, runs in a Lambda, ~14 seconds for a million IPs.

$ docker run -e VRA_KEY -v $(pwd):/data vra/audit audit --input /data/ips.csv
vra audit · streaming variant
0 IPs sent
Run
$ vra audit --input ips.csv --output results.csv

▸ Loaded 1,000,000 IPs from ips.csv
▸ Phase 2 · blind locally ................. 4.8s
▸ Phase 3 · VRA evaluates blinded points .. 2.9s
▸ Phase 4a · unblind locally ............... 3.1s
▸ Phase 4b · streaming lookup .............. 2.0s
▸ Assembling results ..................... 0.4s

✓ Audit complete in 13.2s
✓ 0 IPs transmitted to VRA in cleartext
✓ Wrote results.csv (300,247 graded · 699,753 unmatched)
✓ Peak memory: 62 MB (Lambda-safe)
results.csv 300k matches
ip,grade,category,confidence
203.0.113.1,A,residential,98.4
198.51.100.5,—,—,—
203.0.113.42,B,residential,87.2
192.0.2.18,D,datacenter,52.1
# ... 999,996 more rows
0 IPs
Sent in cleartext
Cryptographic guarantee
~14s
For 1M IPs
End-to-end audit
256MB
Peak client memory
Runs in Lambda
Apache 2.0
Open-source client
Audited by Trail of Bits
The protocol, in four phases

How you get a grade without us seeing the IP.

Each row of input gets blinded by a random scalar before it ever crosses the wire. VRA does the cryptographic work on the blinded form, sends it back, and your machine unblinds it locally. The math is standard oblivious PRF — same family as private set intersection and password-based key agreement.

— Phase 2
★ Local · in your process

Blind your inputs.

For each IP, draw a random scalar and multiply the curve point by it. The output is information-theoretically uncorrelated with the input — no IP recoverable from any blinded point.

Wall-clock
~5s · 16 cores
Wire
local only
→
— Phase 3
⤳ Over the wire

VRA evaluates blindly.

Send the 1M blinded points to VRA. We multiply by our secret key and return them. We never see the underlying IPs because the blinding factor is yours alone.

Wall-clock
~3s · round trip
Wire
32 MB up · 32 MB down
→
— Phase 4a
★ Local · in your process

Unblind the result.

Multiply each evaluated point by the inverse of your blinding scalar. The result is a deterministic OPRF token for each IP — same input always yields the same token under the same key.

Wall-clock
~3s · 16 cores
Wire
local only
→
— Phase 4b
⤳ Streaming lookup

Stream lookup, get grades.

Send the OPRF tokens to VRA's lookup endpoint. We match against our labeled table and return grades only for matches. Tokens are not invertible to your IPs.

Wall-clock
~2s · round trip
Wire
32 MB up · ~8 MB down
End-to-end: ~14 seconds for 1M IPs · ~105 MB total bandwidth · 0 cleartext IPs transmitted
Read the formal protocol spec →
What's actually disclosed

The exact privacy contract.

Procurement and security teams want a precise statement of what VRA learns and what stays yours. Here's the symmetric answer — no marketing language, no hand-waving. If your threat model needs more, the privacy modes section below covers stricter variants.

★ The contract, in one sentence

Your IPs never leave your network. VRA's view is limited to volume, timing, and — in the default streaming variant — your aggregate match rate against our labeled set. Buyers who need stronger guarantees can opt into per-buyer keys or the original local-table protocol.

◐
What VRA learns
Streaming variant · default mode
  • ◇ That an audit happened — timing, volume, and your authenticated identity for billing and rate-limiting.
  • ◇ Your match rate against our labeled set — what fraction of your batch we have a grade for, in aggregate.
  • ◇ Which OPRF tokens you queried in Phase 4b — but a token is a one-way pseudorandom output, not invertible to the underlying IP.
  • ◇ Aggregate timing patterns across your account — useful for detecting abuse, not useful for reconstructing your audience.
◆
What stays yours
★ Cryptographically protected
  • ✓ Every IP in your batch. The OPRF construction makes recovery from blinded inputs and from PRF outputs information-theoretically infeasible.
  • ✓ Your audience composition — beyond the aggregate match rate, VRA cannot infer who's in your set or what they have in common.
  • ✓ Any IP we haven't separately labeled. The lookup returns "no label" for unmatched tokens — VRA doesn't get a list of your unmatched IPs to enrich later.
  • ✓ Your grade-application logic. Whatever you do with the grades — block lists, dashboards, RFP responses — never crosses the wire.
Quick start

One docker run. Five minutes. First audit.

The audit client ships as a Docker image and as language-native libraries. Same protocol, same wire format, same guarantees — pick whichever fits your build pipeline.

i.

Get an API key.

Same key as the public API — sign up with email, paste the vra_live_* token. The credential is for billing and rate-limiting only; it never decrypts protocol messages.

ii.

Pull the audit client.

docker pull vra/audit:latest or install the language library: pip install vra-audit, go get github.com/vra/audit, cargo add vra-audit.

iii.

Run an audit.

Point the client at a CSV of IPs. It blinds locally, talks to VRA, unblinds, looks up, and writes the result. Streaming variant is the default — switch with --mode local-table if you need stricter privacy.

iv.

Verify the cryptography.

Source is on GitHub. Apache 2.0. Trail of Bits review on file. The protocol spec is published, the test vectors are checked in, and any third party can replicate every grade we issue.

# Pull the audit client
docker pull vra/audit:latest

# Run a streaming audit (default — fastest, lowest memory)
docker run \
  -e VRA_KEY=$VRA_KEY \
  -v $(pwd):/data \
  vra/audit audit \
    --input /data/ips.csv \
    --output /data/results.csv

# Stricter privacy: download the local token table once,
# then run audits offline against it
docker run -v $(pwd):/data vra/audit \
  fetch-tokens --output /data/tokens.bin

docker run -v $(pwd):/data vra/audit audit \
  --input /data/ips.csv \
  --tokens /data/tokens.bin \
  --output /data/results.csv
# pip install vra-audit
from vra_audit import Client

client = Client(api_key=os.environ["VRA_KEY"])

# Streaming variant (default)
results = client.audit(
    ips=open("ips.csv").read().splitlines(),
    mode="streaming",   # or "local-table"
)

for r in results:
    if r.grade in ("D", "F"):
        block(r.ip)

# Audit ran in ~14s for 1M IPs.
# Peak memory: ~64 MB. No IP was sent in cleartext.
// go get github.com/vra/audit
package main

import (
    "os"
    "github.com/vra/audit"
)

func main() {
    client := audit.New(os.Getenv("VRA_KEY"))

    res, err := client.Audit(audit.Request{
        InputFile: "ips.csv",
        Mode:      audit.Streaming,  // or audit.LocalTable
    })
    if err != nil { panic(err) }

    for _, r := range res {
        if r.Grade == "D" || r.Grade == "F" {
            block(r.IP)
        }
    }
}
// cargo add vra-audit
use vra_audit::{Client, Mode};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::from_env()?;

    let results = client
        .audit("ips.csv")
        .mode(Mode::Streaming)  // or Mode::LocalTable
        .run()
        .await?;

    for r in results.iter().filter(|r| matches!(r.grade.as_str(), "D" | "F")) {
        block(&r.ip);
    }
    Ok(())
}
★ The same API key works for the audit client and the public scoring API. There's nothing extra to provision. Switch modes per invocation with --mode.
Privacy modes

Four modes, one client.

Same Docker image, same library, same wire protocol — different cryptographic posture. Streaming is the default because it covers nearly every real audit. The stricter modes are there for buyers whose threat model excludes them.

Local table
--mode local-table
Download the per-epoch labeled token table once (4 GB, ~30s) and do every Phase 4 lookup locally. VRA never sees your match rate, never sees which tokens you queried, never can correlate across audits. Best for hedge fund analysts and longitudinal supplier studies.
Per audit~12s · 64 MB
Peak memory~4 GB
Match-rate disclosureNone
Cross-audit correlationNone
Per-buyer key
--mode per-buyer-sk
Each buyer is issued a unique secret key. Eliminates cross-buyer correlation: even if two buyers query the same IP, the resulting tokens differ. VRA recomputes the master table per buyer; pricing reflects the additional server cost.
Per audit~14s
Peak memory~64 MB
Match-rate disclosureYes
Cross-buyer correlationNone
Mix-net submission
--mode mix-net
Phase 4b queries route through a mix network or oblivious-RAM-style protocol, breaking the timing correlation between Phase 3 and Phase 4b. For buyers whose threat model includes timing analysis. Adds latency; you almost certainly do not need this.
Per audit~25s
Peak memory~64 MB
Timing correlationNone
Operational complexityHigh
When to use which

Public API vs. private audit.

Both produce VRA grades from the same v3.1 methodology. The difference is what crosses the wire — segment IDs and match-files for the API, blinded curve points for the private audit client. Pick the one that matches your data-handling constraints.

Public API
Private audit
What you send Segment ID or match-file Blinded curve points
What VRA can read The match-file (in cleartext) Nothing (cryptographically blinded)
Per-call latency ~84ms p99 ~14s for 1M IPs
Best for Bid-time scoring · public registry lookups First-party audience audits · sensitive cohorts
Bandwidth (1M) ~16 MB upload ~105 MB total
Client Standard HTTP — any language Open-source library, Apache 2.0
Auth Same Bearer token Same Bearer token
Pricing ¢0.01 per scored request ¢0.005 per IP audited
For procurement & security

The questions you should be asking.

Can VRA actually see my IPs at any point in the protocol?

+

No. The blinding step in Phase 2 multiplies each curve point by a random scalar that only your client holds. Without that scalar, the blinded point is information-theoretically uncorrelated with the input — VRA cannot recover the IP no matter how much computational power they apply. After unblinding in Phase 4a, what crosses the wire is a one-way OPRF token, also not invertible to the underlying IP. The only way VRA could see an IP is if you ran a different client that sent the IP in cleartext. The open-source client doesn't.

What's an OPRF, in one paragraph?

+

An oblivious pseudorandom function lets two parties compute F(sk, x) where one party holds sk and the other holds x — and neither learns the other's input. The buyer's x is their IP; VRA's sk is the campaign-scoped secret key. The output is a deterministic pseudorandom token: same IP under the same key always produces the same token, but the token reveals nothing about the IP. Same primitive that backs Apple's Private Relay and Cloudflare's Privacy Pass.

Does the streaming variant leak my match rate? Why is that fine?

+

Yes — VRA learns the count of tokens in your batch that matched our labeled table. For most use cases this is harmless: it's an aggregate count, not per-IP information. If your threat model excludes match-rate disclosure (e.g., longitudinal hedge-fund research where the match rate itself reveals strategy), use --mode local-table. The 4 GB token table downloads once per epoch, and after that VRA sees nothing about any individual audit.

Can I run this in AWS Lambda or Cloud Run?

+

Yes — that's the explicit design target for the streaming variant. Peak memory is ~64 MB for a 1M-IP audit, which fits comfortably in any modern serverless tier (Lambda from 128 MB, Cloud Run from 256 MB). The original protocol with the local token table needs ~4 GB and won't fit serverless without significant provisioning. We default to streaming precisely because most production callers are running in serverless.

The client is open-source — what stops a malicious VRA from quietly serving a bad evaluation?

+

The OPRF protocol includes a verification step: VRA publishes a discrete-log proof that the evaluation was computed with the advertised key. The client checks this proof on every batch — a mis-evaluation fails verification and aborts the audit. The proof itself is short (one curve point and one scalar), and the public key is pinned in the client. There's also a published audit transcript every epoch that anyone can replay against the public key. Trust is anchored in cryptographic verification, not in operational claims.

What happens if my Phase 4b lookup fails mid-protocol?

+

Retry Phase 4b alone. The expensive client-side computation (Phase 2 blinding, Phase 4a unblinding) is deterministic given your input IPs and the epoch's secret key, so the unblinded tokens you've already computed don't change. Retrying just re-sends them to the lookup endpoint. The client handles this automatically with exponential backoff; the practical effect is that a transient outage on our side adds seconds, not a re-run.

How is this priced compared to the public API?

+

The audit client is metered per IP audited, not per HTTP request. Streaming variant: ¢0.005 per IP. Local-table variant: ¢0.003 per IP plus a daily ¢25 charge for the token table download. Per-buyer key and mix-net are quoted on volume. Free-tier audit allowance: 10,000 IPs per month, same workspace as the API free tier.

Can I get the cryptographic audit report?

+

Yes — the Trail of Bits review is published in full, redactions only for specific deployment-config items their team flagged. The protocol spec, test vectors, and reference implementation are also public. If your security team needs a deeper engagement (custom threat model, formal verification of a specific deployment), that's available under our enterprise tier.

Ship it

Audit your first million IPs privately.

Sign up, paste the key, run one docker run. The first 10,000 IPs every month are free, and the cryptographic guarantee is the same on the free tier as on the largest enterprise deployment.

Free tier · 10,000 IPs/mo · Open-source client · Apache 2.0 · Audited