8 min read Updated July 6, 2026

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:

ModeDescriptionCostControlPrivacy
relaySend to Treant relay (default)$0Treant managesHash only
local_onlyStore locally, no Solana$0Full localNo publication
self_payDirect Solana transactions~$0.001/txFull on-chainYour 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

  1. Tamandua creates attestation with incident hash, severity, MITRE technique
  2. Attestation is sent to relay API
  3. Relay queues attestation in batch buffer
  4. Every 30 seconds (or when batch is full), relay publishes to Solana
  5. 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

  1. Tamandua creates attestation locally
  2. Attestation hash is computed and stored in database
  3. No external network call
  4. 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:

  1. Accepts attestation hashes via POST
  2. Queues them in a batch buffer
  3. 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

ProviderFree TierRate LimitNotes
Solana PublicYesLimitedGood for dev
HeliusYes (100k/mo)100 RPSGood balance
QuickNodeTrialVariesEnterprise
TritonCustomCustomHigh volume
Self-hostedYour costYour limitMaximum 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

ScenarioBatch SizeIntervalNotes
Low volume (<100/day)105 minMinimize costs
Medium volume (100-1000/day)5030 secDefault
High volume (>1000/day)10010 secMax efficiency
Real-time required10Individual 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

  1. Check mode configuration
  2. Verify network connectivity
  3. Check wallet balance (self-pay mode)
  4. 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