This guide helps wallet providers integrate Atomic Patent IP registry and atomic swap functionality.
commit_ip — Register a new IP commitment
fn commit_ip(owner: Address, commitment_hash: BytesN<32>) -> u64owner: Address that owns the IP (requires auth)commitment_hash: SHA-256 hash ofsecret || blinding_factor- Returns: Unique IP ID
get_ip — Retrieve IP record
fn get_ip(ip_id: u64) -> IpRecordReturns:
struct IpRecord {
ip_id: u64,
owner: Address,
commitment_hash: BytesN<32>,
timestamp: u64,
revoked: bool,
}list_ip_by_owner — List all IPs owned by an address
fn list_ip_by_owner(owner: Address) -> Vec<u64>transfer_ip — Transfer IP ownership
fn transfer_ip(ip_id: u64, new_owner: Address)revoke_ip — Mark IP as revoked
fn revoke_ip(ip_id: u64)initiate_swap — Seller initiates a patent sale
fn initiate_swap(
token: Address,
ip_id: u64,
seller: Address,
price: i128,
buyer: Address
) -> u64token: Token contract address (e.g., XLM, USDC)- Returns: Swap ID
accept_swap — Buyer accepts and sends payment to escrow
fn accept_swap(swap_id: u64)reveal_key — Seller reveals decryption key
fn reveal_key(
swap_id: u64,
caller: Address,
secret: BytesN<32>,
blinding_factor: BytesN<32>
)cancel_swap — Cancel pending swap
fn cancel_swap(swap_id: u64, canceller: Address)cancel_expired_swap — Buyer cancels expired accepted swap
fn cancel_expired_swap(swap_id: u64, caller: Address)get_swap — Retrieve swap details
fn get_swap(swap_id: u64) -> Option<SwapRecord>import { Contract, SorobanRpc, TransactionBuilder, Networks } from '@stellar/stellar-sdk';
const rpcUrl = 'https://soroban-testnet.stellar.org';
const server = new SorobanRpc.Server(rpcUrl);
// Commit IP
async function commitIP(
registryAddress: string,
ownerKeypair: Keypair,
commitmentHash: Buffer
): Promise<string> {
const contract = new Contract(registryAddress);
const account = await server.getAccount(ownerKeypair.publicKey());
const tx = new TransactionBuilder(account, {
fee: '1000',
networkPassphrase: Networks.TESTNET
})
.addOperation(
contract.call(
'commit_ip',
xdr.ScVal.scvAddress(ownerKeypair.publicKey()),
xdr.ScVal.scvBytes(commitmentHash)
)
)
.setTimeout(30)
.build();
tx.sign(ownerKeypair);
const result = await server.sendTransaction(tx);
return result.hash;
}
// Initiate swap
async function initiateSwap(
swapAddress: string,
tokenAddress: string,
ipId: bigint,
sellerKeypair: Keypair,
price: bigint,
buyerAddress: string
): Promise<bigint> {
const contract = new Contract(swapAddress);
const account = await server.getAccount(sellerKeypair.publicKey());
const tx = new TransactionBuilder(account, {
fee: '1000',
networkPassphrase: Networks.TESTNET
})
.addOperation(
contract.call(
'initiate_swap',
xdr.ScVal.scvAddress(tokenAddress),
xdr.ScVal.scvU64(ipId),
xdr.ScVal.scvAddress(sellerKeypair.publicKey()),
xdr.ScVal.scvI128(price),
xdr.ScVal.scvAddress(buyerAddress)
)
)
.setTimeout(30)
.build();
tx.sign(sellerKeypair);
const result = await server.sendTransaction(tx);
// Parse swap_id from result
return parseSwapId(result);
}from stellar_sdk import Soroban, Keypair, Network, TransactionBuilder
from stellar_sdk.soroban_rpc import SorobanServer
rpc_url = "https://soroban-testnet.stellar.org"
server = SorobanServer(rpc_url)
def commit_ip(registry_address: str, owner_keypair: Keypair, commitment_hash: bytes) -> str:
contract = Soroban.Contract(registry_address)
source = server.load_account(owner_keypair.public_key)
tx = (
TransactionBuilder(source, Network.TESTNET_NETWORK_PASSPHRASE, base_fee=1000)
.append_invoke_contract_function_op(
contract_id=registry_address,
function_name="commit_ip",
parameters=[
Soroban.to_address(owner_keypair.public_key),
Soroban.to_bytes(commitment_hash)
]
)
.set_timeout(30)
.build()
)
tx.sign(owner_keypair)
response = server.send_transaction(tx)
return response.hash
def accept_swap(swap_address: str, buyer_keypair: Keypair, swap_id: int) -> str:
contract = Soroban.Contract(swap_address)
source = server.load_account(buyer_keypair.public_key)
tx = (
TransactionBuilder(source, Network.TESTNET_NETWORK_PASSPHRASE, base_fee=1000)
.append_invoke_contract_function_op(
contract_id=swap_address,
function_name="accept_swap",
parameters=[Soroban.to_uint64(swap_id)]
)
.set_timeout(30)
.build()
)
tx.sign(buyer_keypair)
response = server.send_transaction(tx)
return response.hash- User enters IP description/document
- Wallet generates
secret = sha256(document) - Wallet generates random
blinding_factor - Wallet computes
commitment_hash = sha256(secret || blinding_factor) - Wallet stores
secretandblinding_factorsecurely (encrypted local storage) - Wallet calls
commit_ip(user_address, commitment_hash) - Display IP ID and timestamp to user
- User selects IP from their portfolio
- User enters price and buyer address
- Wallet calls
initiate_swap(token, ip_id, seller, price, buyer) - Display swap ID and status
- User views pending swap details
- Wallet shows price and IP metadata
- User confirms payment
- Wallet calls
accept_swap(swap_id)(transfers payment to escrow) - Display "Waiting for seller to reveal key"
- Seller views accepted swap
- Wallet retrieves stored
secretandblinding_factor - Wallet calls
reveal_key(swap_id, seller, secret, blinding_factor) - Payment released to seller
- Display "Swap completed"
- Never expose secrets: Store
secretandblinding_factorencrypted - Verify commitment hashes: Before revealing, confirm the hash matches
- Handle expiry: Notify buyers when swaps are near expiry
- Token allowances: Ensure buyer has approved token transfer before
accept_swap - Gas estimation: Pre-simulate transactions to estimate fees
- Network:
testnet - RPC URL:
https://soroban-testnet.stellar.org - Contract addresses: See README deployment status
The API server wraps every external service call (Soroban RPC, databases, price oracles) in a circuit breaker that prevents cascading failures when a dependency becomes unavailable.
┌─────────────────┐
│ CLOSED │◄──────────────────────────────────────────┐
│ (normal flow) │ │
└────────┬────────┘ │
│ failure_threshold consecutive failures │
▼ │
┌─────────────────┐ timeout_secs elapses ┌───────────────┴──┐
│ OPEN │─────────────────────────►│ HALF-OPEN │
│ (rejects calls) │ │ (test requests) │
└─────────────────┘◄──────────────────────── └──────────────────┘
any failure success_threshold
consecutive successes
| Transition | Trigger |
|---|---|
| Closed → Open | failure_threshold consecutive failures |
| Open → HalfOpen | timeout_secs seconds elapse since last failure |
| HalfOpen → Closed | success_threshold consecutive successes (recovery) |
| HalfOpen → Open | Any failure during test requests |
| Parameter | Default | Description |
|---|---|---|
failure_threshold |
5 |
Consecutive failures before opening the circuit |
success_threshold |
2 |
Consecutive successes in HalfOpen state before closing |
timeout_secs |
30 |
Seconds the circuit stays Open before allowing test requests |
half_open_max_calls |
3 |
Maximum concurrent test requests allowed in HalfOpen state |
All metrics are exported in Prometheus format via GET /metrics.
| Metric | Type | Labels | Description |
|---|---|---|---|
circuit_breaker_state_transitions_total |
Counter | service, from, to |
Incremented on every state change |
circuit_breaker_state |
Gauge | service |
Current state: 0=closed, 1=open, 2=half_open |
circuit_breaker_calls_total |
Counter | service |
Total calls attempted through the breaker |
circuit_breaker_calls_rejected_total |
Counter | service, state |
Calls rejected because the circuit is open |
Each external service gets its own named circuit breaker so failures are isolated:
use api_server::circuit_breaker::{CircuitBreaker, CircuitBreakerConfig};
let oracle_cb = CircuitBreaker::new(
"price-oracle",
CircuitBreakerConfig {
failure_threshold: 5,
success_threshold: 2,
timeout_secs: 30,
half_open_max_calls: 3,
},
);
let db_cb = CircuitBreaker::new(
"postgres",
CircuitBreakerConfig::default(), // same defaults
);The call method wraps a closure and automatically records success or failure:
use api_server::circuit_breaker::CallError;
let result = oracle_cb.call(|| fetch_price_from_oracle());
match result {
Ok(price) => { /* use price */ }
Err(CallError::CircuitOpen) => {
// Oracle circuit is open — return cached value or degrade gracefully
}
Err(CallError::ServiceError(e)) => {
// Oracle responded but returned an error
}
}Configure alerts on these Prometheus expressions:
# Circuit has been open for more than 60 seconds
circuit_breaker_state{service="price-oracle"} == 1
# Rejection rate exceeds 10% of all attempts
rate(circuit_breaker_calls_rejected_total[1m])
/ rate(circuit_breaker_calls_total[1m]) > 0.1
# More than 3 state transitions per minute (thrashing)
rate(circuit_breaker_state_transitions_total[1m]) > 3
Real-time events (ipCommitted, swapInitiated, swapCompleted, categoryAssigned,
swapStatusChanged) are delivered over the graphql-transport-ws subprotocol at
GET /graphql/ws. Connect with a standard GraphQL WebSocket client (graphql-ws npm package,
Apollo Client, etc.) — see the example subscription documents on SubscriptionRoot in
api-server/src/graphql.rs.
By default the API server broadcasts subscription events through an in-process channel only: a
client connected to instance A will not see an event whose triggering contract mutation was
observed solely by instance B. Set the REDIS_URL environment variable (e.g.
redis://localhost:6379) to enable cross-instance delivery — every instance pointed at the same
Redis then fans events out to WebSocket clients connected to any of them. If REDIS_URL is set but
Redis is unreachable at startup, the server logs a warning and falls back to single-instance delivery
rather than failing to start.
Every subscription accepts an optional since argument — the timestamp of the last event the
client successfully processed (every event payload already includes timestamp). The guarantee:
- With
REDIS_URLconfigured: events with a later timestamp thansince, still within the retention window (the last ~500 events per event type), are replayed once before live delivery resumes. This is at-least-once within the window — a client that reconnects after a longer gap, or after more than ~500 events of that type occurred, will have a gap with no delivery guarantee. - Without
REDIS_URL(single-instance mode):sinceis silently ignored — there is no persisted buffer to replay from, so a reconnecting client only receives events from the moment it resubscribes, exactly as ifsincehad been omitted.
subscription {
ipCommitted(owner: "GABC123", since: 1735000000000) {
ipId owner timestamp
}
}- GitHub Issues: https://github.com/AtomicIP/AtomicIP-/issues
- Documentation: https://github.com/AtomicIP/AtomicIP-/tree/main/docs
Atomic Patent API ships with OpenTelemetry (OTel) instrumentation that exports spans via the OTLP protocol. Any OTLP-compatible backend works out of the box: Jaeger, Datadog Agent, Grafana Tempo, Honeycomb, OpenTelemetry Collector, etc.
| Variable | Default | Description |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4317 |
gRPC OTLP endpoint of your backend or Collector |
OTEL_SERVICE_NAME |
atomic-patent-api |
Service name that appears in trace UIs |
OTEL_ENABLED |
true |
Set to false to disable OTel and use plain JSON logs only |
RUST_LOG |
info |
Log level for the tracing subscriber (e.g. debug, info,api_server=debug) |
# 1. Start Jaeger with OTLP gRPC enabled
docker run -d --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
jaegertracing/all-in-one:latest \
--collector.otlp.enabled=true
# 2. Run the API server — it will auto-discover the local Jaeger instance
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
OTEL_SERVICE_NAME=atomic-patent-api \
cargo run -p api-server
# 3. Open the Jaeger UI
open http://localhost:16686# 1. Start the Datadog Agent with OTLP intake enabled
docker run -d --name dd-agent \
-e DD_API_KEY=<YOUR_DD_API_KEY> \
-e DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317 \
-p 4317:4317 \
gcr.io/datadoghq/agent:latest
# 2. Run the API server
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
OTEL_SERVICE_NAME=atomic-patent-api \
cargo run -p api-server# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
datadog:
api:
key: ${DD_API_KEY}
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlp/jaeger, datadog] # fan-out to multiple backendsdocker run --rm -v $(pwd)/otel-collector-config.yaml:/etc/otel/config.yaml \
-p 4317:4317 \
otel/opentelemetry-collector-contrib:latest \
--config /etc/otel/config.yamlEvery HTTP request creates a root server span. Contract operations appear as child spans with domain-specific attributes:
HTTP POST /ip/commit ← root span (HTTP semantic conventions)
└── ip.commit_ip ← child span
ip.owner = "GABCD..."
ip.commitment_hash = "deadbeef..."
HTTP POST /swap/initiate
└── swap.initiate
swap.ip_id = 42
swap.seller = "GABCD..."
swap.buyer = "GXYZ..."
HTTP POST /batch
└── batch.commit
batch.size = 10
└── batch.escrow
batch.ip_count = 5
The API propagates trace correlation via standard headers. Include these in outbound calls and inspect them in responses for end-to-end tracing.
| Header | Direction | Description |
|---|---|---|
traceparent |
request + response | W3C Trace Context (trace-id + span-id). Used by OTel SDK for distributed context propagation. |
X-Trace-ID |
request + response | Human-readable trace UUID. Use this to search in Jaeger / Datadog. |
X-Span-ID |
request + response | Current span UUID. Pass as X-Span-ID on the next outbound call to link parent → child spans. |
Example — propagating trace context between services:
// Service A calls Service B, forwarding the trace context.
const response = await fetch('https://api.atomicpatent.io/ip/commit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Forward context from the upstream request received by Service A:
'traceparent': upstreamRequest.headers.get('traceparent') ?? '',
'X-Trace-ID': upstreamRequest.headers.get('X-Trace-ID') ?? '',
// Service A's current span becomes the parent for Service B's span:
'X-Span-ID': serviceACurrentSpanId,
},
body: JSON.stringify({ owner, commitment_hash }),
});
// The trace_id in Service B's response will match Service A's trace_id.
const traceId = response.headers.get('X-Trace-ID');OTEL_ENABLED=false cargo run -p api-serverWhen disabled, the server falls back to structured JSON logs via
tracing-subscriber without any OTLP export. All tracing::info!/warn!/error!
calls still work normally.