Private Set Intersection Tutorial
Estimated Time to Complete: 20-30 minutes
Expected Outcome
By completing this tutorial, you will:
- Build a complete privacy-preserving private set intersection system with three agents
- Implement AP3 commitments to advertise data structures without revealing values
- Configure agent cards with AP3 extensions for discovery and compatibility checking
- Use Privacy Intent and Result Directives to structure secure computations
Use Case: Quick Commerce Delivery Partner Verification
In the quick commerce industry, companies need to verify if a delivery partner is blacklisted by another company based on their actions before onboarding. However, both parties want to keep their information private:
- Company A (Initiator): Wants to check if a delivery person is blacklisted
- Company B (Receiver): Holds the confidential blacklist
Challenge: Determine if a delivery person appears in the blacklist without revealing the blacklist contents to Company A using Private Set Intersection (PSI) protocol.
System Architecture
The system consists of three agents:
- Company A Agent (
ap3_initiator)- Role: Initiates PSI checks for delivery persons
- Holds: Delivery person queries (name, ID, address)
- Company B Agent (
ap3_receiver)- Role: Processes PSI requests against blacklist
- Holds: Confidential blacklist (never revealed)
- Host Agent (Routing Agent)
- Role: Coordinates agent discovery and protocol initiation
- Provides: AP3 compatibility checking and user interface
Prerequisites
Before starting, ensure you have:
- Python 3.13 installed
- Google AI Studio API Key - Create one here
- Git installed (to clone the repository)
- Basic terminal/command line knowledge
Tutorial Steps
Step 1: Clone and Set Up the Project
# Clone the repository
git clone https://github.com/silence-laboratories/ap3.git
cd ap3/examples/psi
# Set up environment variables
cp .env.example .env
# Edit .env and set GOOGLE_API_KEY=<your-api-key>
# Install dependencies
uv sync && source .venv/bin/activate
Environment Setup
The .env file is loaded automatically by each agent via python-dotenv. You do not need to export environment variables in each terminal — setting them once in .env is sufficient.
Note
In this example, you'll find multiple agents with different roles and commitments. You can explore how we check the compatibility between the agents and how we use the privacy directives to structure the secure computations.
Step 2: Define AP3 Commitments
Both agents need to advertise their data structure using AP3 commitments. Let's implement this:
2.1 Company A Agent Commitment
In company_a_agent/__main__.py, you'll create a commitment like this:
from ap3.types.core import (
CommitmentMetadata,
DataStructure,
DataFormat,
DataFreshness,
Industry,
)
from datetime import datetime
# Company A Initiator Commitment
initiator_commitment = CommitmentMetadata(
commitment_id="psi_initiator_customer_data_v1",
data_structure=DataStructure.CUSTOMER_LIST,
data_format=DataFormat.STRUCTURED,
entry_count=0, # Will vary based on customer data
field_count=3, # name, id, address
estimated_size_mb=0.001,
last_updated=datetime.now().isoformat(),
data_freshness=DataFreshness.REAL_TIME,
industry=Industry.FINANCE,
)
What this does:
- Advertises that Company A has customer data with 3 fields (name, id, address)
- Uses structured data format (can be JSON/CSV)
- Indicates real-time data freshness
- Belongs to the finance industry
- Does NOT reveal the actual customer data or queries
2.2 Company B Agent Commitment
Similarly, in company_b_agent/__main__.py:
# Company B Receiver Commitment
receiver_commitment = CommitmentMetadata(
commitment_id="psi_receiver_sanction_list_v1",
data_structure=DataStructure.BLACKLIST,
data_format=DataFormat.STRUCTURED,
entry_count=16, # Based on default sanction list
field_count=3, # name, id, address
estimated_size_mb=0.001,
last_updated=datetime.now().isoformat(),
data_freshness=DataFreshness.DAILY,
industry=Industry.FINANCE,
)
What this does:
- Advertises that Company B has a blacklist with 16 entries
- Each entry has 3 fields (name, id, address)
- Uses structured data format
- Indicates daily data freshness
- Does NOT reveal the actual blacklist contents
Step 3: Extend Agent Cards with AP3 Parameters
Both agents include AP3 extension parameters in their agent cards:
3.1 Company A Agent Card Extension
# Company A Agent Card Extension
company_a_ap3_extension = {
"uri": "https://github.com/silence-laboratories/ap3/tree/main",
"params": {
"roles": ["ap3_initiator"], # This agent initiates PSI checks
"supported_operations": ["PSI"], # Private Set Intersection
"commitments": [initiator_commitment]
}
}
3.2 Company B Agent Card Extension
# Company B Agent Card Extension
company_b_ap3_extension = {
"uri": "https://github.com/silence-laboratories/ap3/tree/main",
"params": {
"roles": ["ap3_receiver"], # This agent processes PSI requests
"supported_operations": ["PSI"],
"commitments": [receiver_commitment]
}
}
Key Points:
- Roles must be compatible:
ap3_receiver+ap3_initiatorpairs work together - Operations must match: Both must support
PSIfor Private Set Intersection - Commitments enable discovery: They help agents find suitable partners
Step 4: Implement AP3 Discovery
The host agent uses AP3DiscoveryService to:
- Fetch agent cards from both Company A and Company B
- Extract AP3 extension parameters from each card
- Calculate compatibility score based on:
- Role compatibility (receiver + initiator = compatible)
- Common operations (both support PSI)
- Commitment alignment (data structure, format, industry)
4.1 Run the Host Agent
cd host_agent
uv run .
The host agent will start on http://0.0.0.0:8080
4.2 Check Compatibility
The host agent provides an endpoint or interface to check compatibility:
from ap3.discovery import AP3DiscoveryService
ap3_discovery = AP3DiscoveryService()
# Host Agent - Compatibility Check
is_compatible, score, explanation, details = await ap3_discovery.check_compatibility(
agent_a_url="http://localhost:10002",
agent_b_url="http://localhost:10003"
)
print(f"Compatible: {is_compatible}")
print(f"Score: {score}")
print(f"Explanation: {explanation}")
Expected Output:
- is_compatible: True (if roles and operations match)
- score: 1.0 (float between 0 and 1; compatible if >= 0.7)
- Explanation of why they're compatible
Step 5: Use Privacy Directives
5.1 Privacy Intent Directive
When initiating the PSI protocol, Company A creates a PrivacyIntentDirective:
from ap3.types.directive import PrivacyIntentDirective
from datetime import datetime, timedelta, timezone
import uuid
session_id = str(uuid.uuid4())
expiry_time = datetime.now(timezone.utc) + timedelta(hours=1)
privacy_intent = PrivacyIntentDirective(
ap3_session_id=session_id,
intent_directive_id=str(uuid.uuid4()),
operation_type="PSI",
participants=["http://localhost:10002", "http://localhost:10003"],
expiry=expiry_time.isoformat(),
)
What this does: - Establishes a session for the computation - Declares the operation type (PSI = Private Set Intersection) - Lists all participating agents - Sets an expiry time for security
5.2 Validate Privacy Intent
Company B validates this directive before processing:
from common.ap3_directives import validate_privacy_intent_directive
is_valid, error_msg = validate_privacy_intent_directive(
directive=privacy_intent,
expected_operation_type="PSI"
)
if not is_valid:
raise ValueError(f"Invalid privacy intent: {error_msg}")
5.3 Privacy Result Directive
Upon completion, Company A creates a PrivacyResultDirective with the encrypted result:
from ap3.types.directive import PrivacyResultDirective
from ap3.types.core import ResultData, OperationProofs
privacy_result = PrivacyResultDirective(
ap3_session_id=session_id,
result_directive_id=str(uuid.uuid4()),
result_data=ResultData(
encrypted_result=encrypted_psi_result,
result_hash=result_hash,
metadata={
"delivery_person_query": delivery_person_query,
"match_found": str(is_match)
}
),
proofs=OperationProofs(
correctness_proof=correctness_proof,
privacy_proof=privacy_proof,
verification_proof=verification_proof
)
)
Step 6: Run the Complete System
You can run the system either locally or via Docker.
Option A — Local
Run each agent in a separate terminal from the examples/psi directory. Make sure you have completed Step 1 (.env set up and uv sync run) before proceeding.
Terminal 1 — Company B (PSI Receiver):
cd examples/psi/company_b_agent && uv run .
The Company B agent will start on http://localhost:10003
Terminal 2 — Company A (PSI Initiator):
cd examples/psi/company_a_agent && uv run .
The Company A agent will start on http://localhost:10002
Terminal 3 — Host Agent:
cd examples/psi/host_agent && uv run .
The host agent will start on http://localhost:8080
When adding receivers in the UI, use http://localhost:10003.
Option B — Docker
Docker runs all agents in containers. On Apple Silicon Macs, Rosetta must be enabled because the PSI binaries are compiled for linux/amd64.
Apple Silicon only
Open Docker Desktop → Settings → General → enable "Use Rosetta for x86_64/amd64 emulation on Apple Silicon" → Apply and restart.
cd examples/psi
# 1. Set up environment (if not done already)
cp .env.example .env
# Edit .env and set GOOGLE_API_KEY=<your-api-key>
# 2. Build images (first time or after code changes)
make build
# 3. Start all agents
make up
# 4. Open the UI
open http://localhost:8080
When adding receivers in the UI, use Docker service names — not localhost:
http://receiver-b:10003http://receiver-c:10004
Useful make targets:
| Command | Description |
|---|---|
make build |
Build (or rebuild) Docker images |
make up |
Start all agents (detached) |
make down |
Stop all agents |
make logs |
Stream logs from all agents |
make ps |
Show running agent status |
make restart |
Restart all agents |
make clean |
Stop agents and remove volumes (resets receiver selection) |
6.4 Initiate the Computation
Use the host agent's interface to:
- Check compatibility between Company A and Company B agents
- Initiate the PSI computation
- Observe the 2-round protocol execution
- View the final result (MATCH FOUND/NO MATCH) without seeing the blacklist
Expected Flow:
- Host agent checks compatibility
- Company A creates
PrivacyIntentDirective→ sends msg1 (with directive embedded) to Company B - Company B validates directive, processes msg1, sends msg2 back to Company A
- Company A processes msg2 to compute the final boolean result
- Company A creates
PrivacyResultDirectivewith the result - Result shows MATCH FOUND or NO MATCH (without revealing blacklist)
Step 7: Understand the Protocol Flow
The PSI protocol consists of 2 rounds of cryptographic computation:
-
Round 1 (msg1):
- Company A initiator hashes the delivery person query:
delivery_person_hash = hash("DeliveryPerson", query) - Creates PSI message 1 with the hash
- Includes
PrivacyIntentDirectivein the message - Sends to Company B receiver
- Company A initiator hashes the delivery person query:
-
Round 2 (msg2):
- Company B receiver validates the
PrivacyIntentDirective - Hashes all blacklist entries:
blacklist_hashes = [hash("Blacklist", entry) for entry in list] - Performs private set intersection computation
- Returns
msg2(encrypted PSI response) to Company A — Company B does not learn the match result - Blacklist never revealed to Company A
- Company B receiver validates the
-
Result:
- Company A initiator processes
msg2via the PSI operation to get the final boolean result - Company A creates a
PrivacyResultDirectivecapturing the result with cryptographic proofs - Returns: "MATCH FOUND" or "NO MATCH"
- Company A initiator processes
Key Insight:
- Company A never sees Company B's blacklist contents
- The computation happens on hashed/encrypted data
- Only the final match/no-match result is revealed
- Company B's blacklist remains completely private
Summary
In this tutorial, you:
- Set up a three-agent system for privacy-preserving private set intersection
- Implemented AP3 commitments to advertise data structures
- Configured agent cards with AP3 extensions
- Used discovery service to check agent compatibility
- Implemented Privacy Intent and Result Directives
- Executed a secure PSI computation over 2 rounds