Self-Hosted Relay
Where relay mode is deployed and validated, a relay can publish bounded attestation metadata. Self-hosted operators can choose their own attestation strategy; this guide covers available modes and how to deploy your own relay for maximum control.
Attestation Modes
Self-hosted Tamandua instances support three attestation modes:
| Mode | Description | Cost | Control | Privacy |
|---|---|---|---|---|
relay | Send to Treant relay (default) | $0 | Treant manages | Hash only |
local_only | Store locally, no Solana | $0 | Full local | No publication |
self_pay | Direct Solana transactions | ~$0.001/tx | Full on-chain | Your wallet |
Mode: Relay (Default)
The relay mode sends attestation hashes to Treant's relay API, which batches them and publishes to Solana. This is the recommended mode for most self-hosted deployments.
Configuration
# config/runtime.exs
config :tamandua_server, TamanduaServer.Solana.AttestationMode,
mode: :relay,
relay_url: "https://relay.tamandua.treantlab.org/api/v1/attestations",
relay_api_key: System.get_env("TAMANDUA_RELAY_API_KEY")
How It Works
- Tamandua creates attestation with incident hash, severity, MITRE technique
- Attestation is sent to relay API
- Relay queues attestation in batch buffer
- Every 30 seconds (or when batch is full), relay publishes to Solana
- One transaction contains up to 50 attestation hashes
Economics
Self-hosted --> hash --> Relay API --> batch (50 hashes) --> 1 Solana tx
Cost per tx: ~$0.001
Hashes per tx: 50
Cost per hash: $0.00002
1000 operators: $2/month total (Treant subsidized)
Benefits
- Zero cost for operators
- Optional shared metadata - bounded proof metadata can support future aggregate signals
- Publication verification - attestations can be verified on Solana when publication is enabled
- Fallback - automatically stores locally if relay is unreachable
Mode: Local Only
Air-gapped deployments or operators who don't want any external communication can use local-only mode.
Configuration
# config/runtime.exs
config :tamandua_server, TamanduaServer.Solana.AttestationMode,
mode: :local_only
What Happens
- Tamandua creates attestation locally
- Attestation hash is computed and stored in database
- No external network call
- Proofs are verifiable via hash comparison, but not public
Use Cases
- Classified environments
- Regulatory restrictions on external data
- Air-gapped networks
- Development/testing
Limitations
- No public proof of attestation
- Cannot participate in bounty ecosystem
- No contribution to threat intel network
- Manual verification required
Mode: Self-Pay
Full sovereignty mode where you control your own Solana wallet and pay transaction fees directly.
Keypair Setup
# Generate new Solana keypair
solana-keygen new -o ~/.config/solana/tamandua.json
# Fund the wallet (devnet)
solana airdrop 2 $(solana-keygen pubkey ~/.config/solana/tamandua.json) --url devnet
# Fund the wallet (mainnet)
# Transfer SOL from exchange or another wallet
Configuration
# config/runtime.exs
config :tamandua_server, TamanduaServer.Solana.AttestationMode,
mode: :self_pay
config :tamandua_server, TamanduaServer.Solana.Client,
enabled: true,
rpc_url: System.get_env("SOLANA_RPC_URL", "https://api.devnet.solana.com"),
keypair_path: System.get_env("SOLANA_KEYPAIR_PATH", "~/.config/solana/tamandua.json"),
attestation_mode: "memo" # or "anchor" when available
Environment Variables
# .env
SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
SOLANA_KEYPAIR_PATH=/etc/tamandua/solana-keypair.json
Cost Management
With self-pay, you control costs directly:
# config/runtime.exs - only attest high-severity
config :tamandua_server, TamanduaServer.Alerts,
auto_attest: true,
attest_severity_threshold: "high" # only high/critical
# Alternative: batch your own attestations
config :tamandua_server, TamanduaServer.Solana.RelayBatch,
enabled: true,
batch_size: 50,
batch_interval_ms: 60_000 # 1 minute
Security Considerations
- Protect your keypair - treat it like a private key
- Limit SOL balance - only keep what's needed for fees
- Monitor usage - set up alerts for unexpected transactions
- Rotate regularly - consider periodic key rotation
Deploy Your Own Relay
For organizations wanting full control over batching and publication, you can deploy your own relay.
Architecture
[Your Tamandua Instances] --> [Your Relay Server] --> [Solana]
| |
v v
[Local Storage] [Batch Queue]
Relay Server Setup
The relay is a simple HTTP server that:
- Accepts attestation hashes via POST
- Queues them in a batch buffer
- Publishes batches to Solana on interval or when full
Minimal Relay Implementation
defmodule MyRelay do
use GenServer
@batch_size 50
@batch_interval_ms 30_000
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def queue_attestation(attestation) do
GenServer.call(__MODULE__, {:queue, attestation})
end
@impl true
def init(_opts) do
schedule_flush()
{:ok, %{queue: :queue.new()}}
end
@impl true
def handle_call({:queue, attestation}, _from, state) do
new_queue = :queue.in(attestation, state.queue)
state = %{state | queue: new_queue}
# Flush if batch full
state = if :queue.len(state.queue) >= @batch_size do
flush_batch(state)
else
state
end
{:reply, :ok, state}
end
@impl true
def handle_info(:flush, state) do
state = flush_batch(state)
schedule_flush()
{:noreply, state}
end
defp flush_batch(state) do
{batch, remaining} = take_batch(state.queue, @batch_size)
if length(batch) > 0 do
memo = build_batch_memo(batch)
{:ok, signature} = TamanduaServer.Solana.Client.submit_memo(memo)
Logger.info("Published batch: #{length(batch)} attestations, tx=#{signature}")
end
%{state | queue: remaining}
end
defp take_batch(queue, count) do
take_batch(queue, count, [])
end
defp take_batch(queue, 0, acc), do: {Enum.reverse(acc), queue}
defp take_batch(queue, count, acc) do
case :queue.out(queue) do
{{:value, item}, new_queue} ->
take_batch(new_queue, count - 1, [item | acc])
{:empty, queue} ->
{Enum.reverse(acc), queue}
end
end
defp build_batch_memo(batch) do
hashes = Enum.map(batch, & &1.incident_hash)
Jason.encode!(%{
t: "tamandua_batch",
v: 1,
n: length(batch),
h: hashes,
ts: DateTime.utc_now() |> DateTime.to_unix()
})
end
defp schedule_flush do
Process.send_after(self(), :flush, @batch_interval_ms)
end
end
Relay API Endpoints
# router.ex
scope "/api/v1", MyRelayWeb do
pipe_through :api
post "/attestations", AttestationController, :create
get "/status", StatusController, :show
end
# attestation_controller.ex
defmodule MyRelayWeb.AttestationController do
use MyRelayWeb, :controller
def create(conn, %{"attestation" => params}) do
:ok = MyRelay.queue_attestation(params)
conn
|> put_status(:accepted)
|> json(%{status: "queued"})
end
end
Relay Configuration on Instances
Point your Tamandua instances to your relay:
# config/runtime.exs
config :tamandua_server, TamanduaServer.Solana.AttestationMode,
mode: :relay,
relay_url: "https://your-relay.internal/api/v1/attestations",
relay_api_key: System.get_env("YOUR_RELAY_API_KEY")
RPC Configuration
Choosing an RPC Provider
| Provider | Free Tier | Rate Limit | Notes |
|---|---|---|---|
| Solana Public | Yes | Limited | Good for dev |
| Helius | Yes (100k/mo) | 100 RPS | Good balance |
| QuickNode | Trial | Varies | Enterprise |
| Triton | Custom | Custom | High volume |
| Self-hosted | Your cost | Your limit | Maximum control |
Configuration Examples
# Devnet (development)
config :tamandua_server, TamanduaServer.Solana.Client,
rpc_url: "https://api.devnet.solana.com"
# Mainnet with Helius
config :tamandua_server, TamanduaServer.Solana.Client,
rpc_url: "https://mainnet.helius-rpc.com/?api-key=YOUR_KEY"
# Self-hosted validator
config :tamandua_server, TamanduaServer.Solana.Client,
rpc_url: "http://your-validator:8899"
Batch Settings
Tune batching for your volume:
config :tamandua_server, TamanduaServer.Solana.RelayBatch,
enabled: true,
batch_size: 50, # Max attestations per tx (Solana limit ~50-100)
batch_interval_ms: 30_000, # Flush interval
max_queue_size: 10_000 # Buffer limit
Recommended Settings
| Scenario | Batch Size | Interval | Notes |
|---|---|---|---|
| Low volume (<100/day) | 10 | 5 min | Minimize costs |
| Medium volume (100-1000/day) | 50 | 30 sec | Default |
| High volume (>1000/day) | 100 | 10 sec | Max efficiency |
| Real-time required | 1 | 0 | Individual txs |
Cost Management
Monitoring
# Get relay status
TamanduaServer.Solana.RelayBatch.status()
# %{
# enabled: true,
# queue_size: 12,
# stats: %{
# total_queued: 1234,
# total_published: 1200,
# total_batches: 24
# }
# }
Estimating Costs
Monthly attestations: 10,000
Batch size: 50
Batches needed: 200
Cost per tx: ~$0.001
Monthly cost: ~$0.20
With 1 SOL (~$100):
- 100,000 transactions
- 5,000,000 attestations (at batch size 50)
Budget Alerts
Set up balance monitoring:
# Check wallet balance
solana balance ~/.config/solana/tamandua.json --url mainnet-beta
# Script for alerts
#!/bin/bash
BALANCE=$(solana balance ~/.config/solana/tamandua.json --url mainnet-beta | cut -d' ' -f1)
if (( $(echo "$BALANCE < 0.1" | bc -l) )); then
echo "Low balance alert: $BALANCE SOL"
# Send notification
fi
Troubleshooting
Attestation Not Published
- Check mode configuration
- Verify network connectivity
- Check wallet balance (self-pay mode)
- Review logs for errors
# Check Solana client status
curl http://localhost:4000/api/v1/solana/status
# View attestation queue
curl http://localhost:4000/api/v1/solana/relay/status
RPC Errors
# Common errors
{:error, {:rpc_error, "429 Too Many Requests"}}
# -> Rate limited, use better RPC or reduce volume
{:error, {:rpc_error, "blockhash not found"}}
# -> Network congestion, retry with backoff
{:error, {:http_error, 503, _}}
# -> RPC down, failover to backup
Keypair Issues
# Verify keypair
solana-keygen verify ~/.config/solana/tamandua.json
# Test transaction
solana transfer --fee-payer ~/.config/solana/tamandua.json \
<YOUR_OTHER_WALLET> 0.001 --url devnet --allow-unfunded-recipient
Migration Guide
From Local-Only to Relay
# 1. Update config
config :tamandua_server, TamanduaServer.Solana.AttestationMode,
mode: :relay
# 2. Restart application
# 3. New attestations will use relay
# 4. Historical attestations remain local-only
From Relay to Self-Pay
# 1. Set up keypair
# 2. Fund wallet
# 3. Update config
config :tamandua_server, TamanduaServer.Solana.AttestationMode,
mode: :self_pay
config :tamandua_server, TamanduaServer.Solana.Client,
enabled: true,
keypair_path: "/etc/tamandua/solana-keypair.json"
# 4. Restart application
Next Steps
- Solana Program - Technical program reference
- Verification - Verify your attestations
- Bounty System - Contribute and earn rewards