Skip to content

Configuration

Three-tier configuration with precedence: environment variables > config file > defaults.

SettingSourceHot Reload?Example
Listen addressEnv varNo (restart)EXCHANGE_LISTEN_ADDR=:8080
Exchange domainEnv varNoEXCHANGE_DOMAIN=exchange.ssp.com
TLS cert/keyEnv var / fileNoEXCHANGE_TLS_CERT=/etc/tls/cert.pem
Tenant configsConfig file (YAML/JSON)Yes (SIGHUP)EXCHANGE_CONFIG=/etc/exchange/config.yaml
Signing keysSecrets managerYes (polled)EXCHANGE_SECRETS_PROVIDER=aws-sm
Billing adapter endpointConfig fileYes (SIGHUP)In tenant config
Log levelEnv varYes (signal)EXCHANGE_LOG_LEVEL=info
WAL directoryEnv varNoEXCHANGE_WAL_DIR=/var/lib/exchange/wal
Transaction store DSNEnv varNoEXCHANGE_TXN_STORE_DSN=kafka://...

/etc/exchange/config.yaml
exchange:
domain: "exchange.ssp-example.com"
version: "1.0"
billing:
adapter: "dynamodb" # or "http", "in-memory"
endpoint: "https://billing.internal.ssp.com/v1"
timeout: "30ms"
timeout_policy: "reject" # or "defer"
transaction_log:
store: "kafka"
kafka:
brokers: ["kafka-1:9092", "kafka-2:9092"]
topic: "exchange-transactions"
wal:
dir: "/var/lib/exchange/wal"
max_bytes: 1073741824 # 1 GB
flush_interval: "50ms"
flush_batch_size: 100
tenants:
- id: "hearst-media"
domains:
- "cosmopolitan.com"
- "esquire.com"
- "elle.com"
rsl_urls:
cosmopolitan.com: "https://cosmopolitan.com/rsl.txt"
esquire.com: "https://esquire.com/rsl.txt"
elle.com: "https://elle.com/rsl.txt"
rsl_poll_interval: "5m"
content_base_url: "https://cdn.hearst.com"
cdn_type: "cloudfront"
signing_key_ref: "arn:aws:secretsmanager:us-east-1:123456:secret:hearst-signing-key"
url_ttl_seconds: 300
bind_agent_id: true
reporting: # Tenant-level default — inherited by all offers
required: true
window: "86400s"
required_fields: ["transaction_id", "function", "consumed_quantity"]
abuse_thresholds:
min_query_to_txn_ratio: 0.01 # Application-level only; volumetric limiting at infra
catalog_contributors: # v1.0: authorized third-party push sources
- domain: "doubleverify.com"
relationship: "verifier"
verifier_trust: # v1.0: trusted attestation verifiers
cache_ttl: "1h"
trusted_verifiers:
- "doubleverify.com"
- "gumgum.com"
dispute_thresholds: # v1.0: per-tenant dispute policy
auto_credit_max_amount: 0.50 # Max auto-credit per dispute (USD)
agent_dispute_rate_warning: 0.03 # 3% warning threshold
agent_dispute_rate_block: 0.05 # 5% blocking threshold
- id: "techcrunch-com"
domains: ["techcrunch.com"]
rsl_urls:
techcrunch.com: "https://techcrunch.com/rsl.txt"
# ...

  • Keys stored encrypted at rest: AWS KMS envelope encryption or HashiCorp Vault transit engine
  • Per-tenant key isolation — each tenant’s keys are under a separate KMS key or Vault path
  • Access audit trail on all key operations
ProviderConfig ValueNotes
AWS Secrets Manageraws-smProduction recommended for AWS deployments
HashiCorp VaultvaultFor multi-cloud or on-premises
GCP Secret Managergcp-smFor GCP deployments
Local filefileDevelopment only

The signing_key_ref in tenant configuration points to the key’s location in the secrets provider:

# AWS Secrets Manager
signing_key_ref: "arn:aws:secretsmanager:us-east-1:123456:secret:hearst-signing-key"
# HashiCorp Vault
signing_key_ref: "vault://secret/exchange/tenants/hearst-media/signing-key"
# Local file (development only)
signing_key_ref: "file:///var/lib/exchange/keys/hearst-media.pem"

Keys are loaded from the secrets provider at startup and cached in memory. A background goroutine polls for rotations every 60 seconds.


Tenant configuration changes are reloaded on SIGHUP without restarting the process:

func (s *Server) watchConfig(ctx context.Context) {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP)
for {
select {
case <-ctx.Done():
return
case <-sigCh:
newCfg, err := LoadConfig(s.configPath)
if err != nil {
slog.Error("config reload failed", slog.Any("error", err))
continue
}
s.applyConfig(newCfg) // Updates tenant map, triggers RSL re-ingestion
slog.Info("config reloaded", slog.Int("tenants", len(newCfg.Tenants)))
}
}
}
ComponentTriggerBehavior
Tenant configsSIGHUPTenant map updated, new tenants’ RSL ingestion started
Signing keysBackground poll (60s)New key becomes active, old key moves to previous
Content catalogRSL poll interval (default 5m)Atomic pointer swap of CatalogSnapshot
Billing adapter endpointSIGHUPConnection pool updated
Log levelSIGHUP or env varImmediate effect
ComponentReason
Listen addressBound at startup
TLS certificatesLoaded at startup
WAL directoryFile handles opened at startup
Transaction store DSNConnection established at startup
Exchange domainUsed in response construction

Catalog changes (RSL re-ingestion) happen automatically on the poll interval. An explicit re-ingestion can be triggered via the admin API:

POST /admin/tenants/{tenant_id}/refresh-catalog

Resource ingestion is a background pipeline, separate from request handling. It builds and maintains the resource catalog.

Tier 1 (best): Provider provides token estimates

  • Via RSL word count extension, sitemap XML extension, or CoMP Package.scope.text.wordcount
  • Exchange uses directly, no crawling needed

Tier 2 (good): Exchange crawls and estimates

  • Crawl URLs from sitemap.xml
  • Extract main text using readability algorithm (Mozilla Readability.js equivalent in Go)
  • Count words -> estimate tokens (wordcount x 1.32)
  • Store in catalog, refresh periodically (daily or on provider webhook)

Tier 3 (fallback): Tenant-level defaults

  • Provider sets average word count per content type via tenant management API
  • “My articles average 2000 words” -> estimated_quantity = 2640
  • Crude but functional for unit_cost comparison
1. Crawl sitemap.xml for each tenant domain → discover all content URLs
2. Check for rsl.txt at each domain → parse pricing, restrictions, terms
3. If no rsl.txt: fall back to tenant-level defaults from config
4. Build OfferTemplates from merged data (sitemap URLs + RSL pricing + tenant defaults)
5. Atomic pointer swap into CatalogSnapshot

Providers can add token estimates and content IDs directly in sitemap.xml using XML namespace extensions:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:ramp="https://ramp.ai/sitemap/1.0">
<url>
<loc>https://example.com/premium/article.html</loc>
<lastmod>2026-03-14</lastmod>
<ramp:tokens>3300</ramp:tokens>
<ramp:content-id>article-2026-03-14-ai-infra</ramp:content-id>
<ramp:pricing-model>per_article</ramp:pricing-model>
<ramp:rate currency="USD">0.05</ramp:rate>
</url>
</urlset>

XML namespace extensions don’t break standard sitemap readers — they ignore unknown namespaces. This is established practice (Google uses image:, video:, news: extensions).

For WordPress sites, the Exchange can poll the REST API (/wp-json/wp/v2/posts) to discover content with rich metadata (title, excerpt, word count, categories). This provides better token estimates than sitemap-only discovery. CMS integration is optional and configured per-tenant.

The ingestion pipeline runs as a separate background process (not in the Exchange hot path):

  1. Crawl sitemap.xml -> discover content URLs
  2. For each URL: check for RAMP sitemap extensions -> use if present
  3. If no extensions: crawl HTML -> extract text (readability algorithm) -> count words -> estimate tokens
  4. Check rsl.txt -> merge pricing terms
  5. Build catalog entries with token estimates
  6. Serialize radix trie to binary file
  7. Exchange process loads pre-built binary via atomic pointer swap

Refresh: daily cron, or on provider webhook notification (POST /admin/refresh-catalog). Not on the hot path. Never blocks DiscoverResources or ExecuteTransaction.


ScenarioSentinel ErrorConnect CodegRPC CodeHTTP Status
Invalid request fields(validation)CodeInvalidArgumentINVALID_ARGUMENT400
Offer signature verification failedErrOfferSignatureCodeInvalidArgumentINVALID_ARGUMENT400
Offer expiredErrOfferExpiredCodeNotFoundNOT_FOUND404
Billing denied (general)ErrBillingDeniedCodePermissionDeniedPERMISSION_DENIED403
Billing denied (insufficient balance)ErrBillingBalanceCodeResourceExhaustedRESOURCE_EXHAUSTED429
Reporting obligation outstanding(reporting)CodePermissionDeniedPERMISSION_DENIED403
Application-level abuse (low query:txn ratio)(abuse)CodeResourceExhaustedRESOURCE_EXHAUSTED429
Billing adapter timeoutErrBillingTimeoutCodeUnavailableUNAVAILABLE503
Transaction log full (WAL capacity)ErrTransactionLogCodeUnavailableUNAVAILABLE503
Internal error (signing, catalog)ErrTransactionSigningCodeInternalINTERNAL500
Idempotent replay(none)Success (cached)OK200

Note: Volumetric rate limiting (per-IP, per-tenant RPS) is handled by the infrastructure layer (HAProxy/Envoy/API Gateway) and returns 429 before reaching the Exchange service.