This document describes the design philosophy, module organization, and extension points for the AffixioClient SDK.
The SDK is designed to be fully stateless. Each verification call is independent:
- No session management
- No request caching
- No persistent HTTP connections
- Fresh authentication on every call (Bearer token in Authorization header)
Why: Stateless architecture is easier to horizontally scale, test in isolation, and reason about. Especially important for agentic systems where multiple agents may share the same SDK instance.
Full TypeScript throughout, with zero implicit any:
- Type definitions in dedicated
types.tsfile - All public methods accept typed parameters and return typed responses
- Custom error classes with typed metadata
Why: Catches errors at compile time rather than runtime. IDE autocompletion helps integrators discover and use the API correctly.
The SDK uses only:
- Native Node.js
fetchAPI (v18+) - TypeScript (dev-only)
No Express, axios, or heavy HTTP client libraries. Native fetch is sufficient and keeps the bundle small for Node.js and browser environments.
Why: Faster installation, fewer security updates, easier to audit dependencies.
Instead of forcing integrators to build their own request payloads, the SDK provides high-level helpers:
verifyIssuerAgentAuthorization()- turnkey issuer authverifyMerchantAgentCheckout()- turnkey merchant checkoutverifyAgentConsent()- turnkey consent verificationverifyAgentPaymentComposite()- multi-circuit orchestration
Each helper validates inputs and constructs the request correctly. Integrators don't need to know the exact JSON structure or circuit IDs; the helpers encode domain knowledge.
Why: Reduces errors and documentation overhead. Helpers are easier to version and deprecate than exposing raw circuit IDs.
The structure is designed to be easily ported to other languages:
- Clear separation of concerns (client, http, types, errors, helpers)
- No language-specific idioms in the structure
- Helpers use simple transformations, not metaprogramming
Why: Once the TypeScript SDK is proven, porting to Go, Java, or Python should be straightforward.
src/
├── index.ts # Public API exports
├── client.ts # Main AffixioClient class
├── types.ts # TypeScript type definitions
├── errors.ts # Error classes
├── http.ts # Low-level HTTP abstraction
└── helpers.ts # Request builders and utilities
Contains the main AffixioClient class with:
- Constructor accepting
AffixioClientConfig - Static
fromEnv()factory method for environment-based setup - Public methods:
verify(),verifyIssuerAgentAuthorization(),verifyMerchantAgentCheckout(),verifyAgentConsent(),verifyAgentPaymentComposite()
Each method:
- Validates input using
validateRequired()helper - Builds the request using a helper function
- Calls
this.http.post()to execute - Logs via the configured logger
- Returns the typed response or throws a typed error
Defines all public and internal types:
AffixioClientConfig- client configurationCircuitId- union of known circuits +stringfor extensibilityVerifyRequest<TContext>- generic verification request with typed contextVerifyResponse- response from a single verification- Request/response types for each agentic helper
- Composite types for multi-circuit verification
No logic here; just pure type definitions.
Custom error classes inheriting from a base AffixioError:
AffixioTransportError- network/timeout issues; retryableAffixioApiError- 4xx/5xx responses; includes status and request IDAffixioValidationError- client-side validation; not retryable
All errors carry metadata for logging and debugging.
Low-level HTTP client wrapping native fetch:
- Methods:
post<T>(),get<T>() - Handles:
- Timeout enforcement (AbortController)
- Response parsing (JSON fallback to text)
- Error mapping (network errors → AffixioTransportError, 4xx/5xx → AffixioApiError)
- Request ID propagation (X-Request-ID header)
- Authorization header (Bearer token)
No retries or backoff; that's the integrator's responsibility.
Utility functions used by the client:
buildIssuerAuthRequest()- converts high-level request to generic VerifyRequestbuildMerchantCheckoutRequest()- same for merchant checkoutbuildAgentConsentRequest()- same for consentbuildCompositeContext()- assembles context for composite verificationvalidateRequired()- checks for missing required fieldsaggregateProofs()- combines proofs from multiple circuitsgenerateRequestId()- creates unique request IDs
These are internal helpers but exported for testing and potential manual use.
Public API exports. Only re-exports from other modules:
- Client class
- Error classes
- Type definitions
- Selected helper functions (for advanced use)
Integrators should only import from the main package:
import { AffixioClient, AffixioApiError, ... } from '@affix-io/verification-sdk';Network failures, timeouts, DNS issues:
- Throw
AffixioTransportError - Include original error for debugging
- Metadata includes path, URL, timeout value
- Integrator should implement retry logic with exponential backoff
4xx or 5xx responses:
- Throw
AffixioApiError - Include HTTP status, status text, response body, request ID
status >= 500typically retryablestatus 4xxusually not retryable (validation error, auth error)- Request ID aids support requests and debugging
Missing or invalid client-side fields:
- Throw
AffixioValidationError - Include field name and message
- Fix input and retry with corrected data
To add support for a new agentic payment scenario:
-
Define request type in
types.ts:export interface MyNewVerifyRequest { agentId: string; // ... fields }
-
Define context type in
types.ts:export interface MyNewContext { // ... context fields }
-
Create builder helper in
helpers.ts:export function buildMyNewRequest( req: MyNewVerifyRequest ): VerifyRequest<MyNewContext> { return { circuit_id: 'appropriate-circuit', identifier: req.agentId, context: { /* ... */ }, }; }
-
Add client method in
client.ts:async verifyMyNew(request: MyNewVerifyRequest): Promise<VerifyResponse> { validateRequired(request, ['agentId', /* ... */]); const verifyRequest = buildMyNewRequest(request); return this.verify(verifyRequest); }
-
Export types in
index.ts:export type { MyNewVerifyRequest } from './types.js';
If AffixIO adds a new circuit (e.g., agentic-payment-limit-check):
-
Update the
CircuitIdunion intypes.ts:export type CircuitId = | 'agentic-payment-permission' | 'agentic-payment-limit-check' // <-- new | ...
-
Optionally add a new helper method if it represents a distinct use case.
The generic verify() method already works with any circuit ID, so no changes needed there.
Integrators can provide a custom logger to integrate with their observability stack:
const client = new AffixioClient({
apiKey: process.env.AFFIXIO_API_KEY!,
logger: myCustomLogger,
});The SDK calls:
logger.info()for successful verifications and state changeslogger.warn()for non-fatal issues (e.g., circuit failure in composite)logger.error()for exceptions and API errors
By default, composite verification runs:
agentic-payment-permissionfinance-account-standingfinance-fraud-indicator
Integrators can override with the circuits parameter:
await client.verifyAgentPaymentComposite({
// ...
circuits: ['agentic-payment-permission', 'cross-mfa-verification'],
});For new use cases, add new circuit IDs to the union type and pass them explicitly.
- Mock
fetchglobally to avoid real API calls - Test happy path for each verification method
- Test validation errors (missing fields)
- Test error handling (transport, API, validation)
- Test composite verification logic
Run with npm test.
To test against a real (staging) AffixIO API:
- Set
AFFIXIO_API_KEY=<staging-key> - Create tests that don't mock
fetch - Run against staging environment
Use the examples in examples/full-usage.ts:
AFFIXIO_API_KEY=test_key npx ts-node examples/full-usage.tsDefault timeout is 5 seconds. Configurable:
const client = new AffixioClient({
apiKey: '...',
timeoutMs: 10000, // 10 seconds
});For high-latency integrations, increase timeout. For real-time (e.g., point-of-sale), keep it low and retry on timeout.
The SDK doesn't provide batch verification. Call verify() in parallel if needed:
const results = await Promise.all([
client.verifyIssuerAgentAuthorization(req1),
client.verifyIssuerAgentAuthorization(req2),
client.verifyIssuerAgentAuthorization(req3),
]);Composite proofs are concatenated with : separator for simplicity. For production, consider using a cryptographic hash (SHA-256) over all proofs:
export function aggregateProofs(proofs: string[]): string {
const combined = proofs.join(':');
return crypto.createHash('sha256').update(combined).digest('hex');
}- Webhook support: Server-push verification results for async workflows
- Batch API: POST multiple circuits in one call
- Circuit introspection: GET
/v1/circuitsto discover available circuits and their requirements
- Go:
github.com/affix-io/verification-sdk-go - Python:
pip install affixio-verification-sdk - Java: Maven Central
com.affixio:verification-sdk
- Never commit API keys to version control
- Use environment variables (
AFFIXIO_API_KEY) - Or use a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.)
- All requests use HTTPS (enforced by default
baseUrl) - Bearer token sent in Authorization header (not in URL)
- Proofs are opaque strings generated by the API
- Don't trust proofs from other sources; always call
/v1/verify - Use proofs for audit trails, not authorization decisions
- API error responses may include sensitive data
- Don't log error bodies in production without sanitization
- Always include request ID for debugging with support
- Circuit: A verification function (e.g.,
agentic-payment-permission) - Identifier: Unique ID for the entity being verified (agent, account, card)
- Context: JSON-serializable metadata about the verification (amount, merchant, etc.)
- Proof: Cryptographic proof of the verification result; use for audit trails
- Eligible: Boolean outcome; true if the entity passes the circuit
- Composite: Multi-circuit verification with aggregated result
- Request ID: Unique identifier for a single verification call; use for tracing