For the complete documentation index optimized for AI agents, see llms.txt or llms-full.txt. A markdown version of this page is available by appending.mdto the URL or sendingAccept: text/markdown.
Transaction Relay
For AI agents: the documentation index is at llms.txt (full corpus: llms-full.txt). A markdown source for this page is /a4-server/transaction-relay.md.
arete-server can expose a purpose-built Solana transaction transport on the same HTTP origin as stack reads. It is disabled by default, even when an RPC URL is configured.
The relay provides six fixed routes. It is not a generic JSON-RPC proxy, and it never receives signing keys or signs transactions.
Enable the HTTP server
Section titled “Enable the HTTP server”The relay requires the server’s HTTP listener:
use arete_server::Server;
Server::builder() .spec(my_stack::spec()) .websocket() .bind("[::]:8877".parse()?) .http() .health_monitoring() .start() .await?;Enable transaction routes through environment configuration:
ARETE_TRANSACTIONS_ENABLED=true \ARETE_TRANSACTION_RPC_URL=https://your-dedicated-solana-rpc.example \cargo run --releaseOr configure them programmatically:
use arete_server::{Server, TransactionConfig};
Server::builder() .spec(my_stack::spec()) .websocket() .http() .transactions_config(TransactionConfig { enabled: true, rpc_url: Some(transaction_rpc_url), ..TransactionConfig::default() }) .start() .await?;Routes and scopes
Section titled “Routes and scopes”| Route | Required scope | Solana RPC method |
|---|---|---|
POST /transactions/v1/latest-blockhash | transaction:inspect | getLatestBlockhash |
POST /transactions/v1/fee | transaction:inspect | getFeeForMessage |
POST /transactions/v1/simulate | transaction:inspect | simulateTransaction |
POST /transactions/v1/signature-status | transaction:inspect | getSignatureStatuses |
POST /transactions/v1/signature-statuses | transaction:inspect | getSignatureStatuses |
POST /transactions/v1/block-height | transaction:inspect | getBlockHeight |
POST /transactions/v1/get | transaction:inspect | getTransaction |
POST /transactions/v1/signatures | transaction:inspect | getSignaturesForAddress |
POST /transactions/v1/send | transaction:send | sendTransaction |
Routes accept validated JSON schemas and reject unknown fields. Solana u64 values are decimal strings over HTTP.
signature-statuses resolves up to 256 signatures in one call. Results are positionally aligned
with the request: an unknown signature is null in its own slot rather than omitted, so a caller
cannot attribute one signature’s status to another. An empty batch answers without contacting the
cluster, and reports X-Arete-Upstream-Attempted: false accordingly. More than 256 signatures is
refused as batch_limit_exceeded, which is Solana’s own getSignatureStatuses ceiling.
get answers what a confirmed transaction moved, not how: err, slot, blockTime and every
resolved account’s pre/post lamport balance. Instructions, logs and token balances are out of
scope — a payer that settles inside a CPI is only visible in the balance deltas. The account list
is the cluster’s own resolved order, lookup-table entries included, so it lines up with the
balances position by position. An unseen signature is null with 200, not an error.
maxSupportedTransactionVersion defaults to the highest version the relay can describe (1) and is
a JSON number, not a decimal string; commitment accepts confirmed or finalized.
signatures pages an address’s history, newest first: {address, limit, before, until, commitment} in, [{signature, slot, blockTime, err}] out. before/until are signatures and
exclusive at both ends, limit is a JSON number capped at Solana’s own 1000, and an address with
no history is an empty array rather than an error. Memos and confirmation status are dropped — the
page is a cursor, and a caller that wants detail asks get for the signature.
simulate returns loadedAccountsDataSize alongside unitsConsumed, both decimal strings. It is
what a caller measures before setting a V1 loadedAccountsDataSizeLimit, so a budget can be
derived from the simulation rather than guessed. Absent stays absent: an upstream that does not
report the figure is not reporting zero, and the two must not be confused when sizing a limit.
Authentication
Section titled “Authentication”For production, configure SignedSessionAuthPlugin through the HTTP auth plugin boundary. Read, inspect, and send scopes are independent. A read-only token cannot inspect or submit transactions.
Do not expose a production relay with development allow_all behavior. Self-hosted local development may deliberately omit the plugin when the listener is not publicly reachable.
When running behind a trusted proxy, configure ARETE_TRUSTED_PROXY_CIDRS. Forwarded client IP headers are ignored unless the direct peer belongs to one of those networks.
Configuration reference
Section titled “Configuration reference”| Variable | Default | Purpose |
|---|---|---|
ARETE_TRANSACTIONS_ENABLED | false | Expose transaction routes |
ARETE_TRANSACTION_RPC_URL | unset | Preferred upstream transaction RPC |
SOLANA_RPC_URL / RPC_URL | unset | Legacy transaction RPC fallback |
ARETE_TRANSACTION_MAX_BODY_BYTES | 32768 | Maximum JSON request body |
ARETE_TRANSACTION_MAX_BYTES | 4096 | Maximum decoded message/transaction bytes |
ARETE_TRANSACTION_INSPECT_TIMEOUT_MS | 10000 | Inspection upstream timeout |
ARETE_TRANSACTION_SEND_TIMEOUT_MS | 15000 | Submission upstream timeout |
ARETE_TRANSACTION_STATUS_TIMEOUT_MS | 10000 | Status/block-height timeout |
ARETE_TRANSACTION_INSPECT_CONCURRENCY | 64 | Global inspect concurrency |
ARETE_TRANSACTION_SEND_CONCURRENCY | 16 | Global send concurrency |
ARETE_TRANSACTION_INSPECT_REQUESTS_PER_MINUTE | 600 | Server inspect clamp |
ARETE_TRANSACTION_SEND_REQUESTS_PER_MINUTE | 60 | Server send clamp |
ARETE_TRANSACTION_STATUS_REQUESTS_PER_MINUTE | 600 | Server status clamp |
ARETE_TRUSTED_PROXY_CIDRS | empty | Comma-separated trusted proxy networks |
ARETE_TRANSACTION_USAGE_ENABLED | false | Enable transaction usage delivery |
ARETE_TRANSACTION_USAGE_ENDPOINT | unset | Usage ingestion endpoint |
ARETE_TRANSACTION_USAGE_TOKEN | unset | Usage ingestion bearer token |
ARETE_TRANSACTION_USAGE_SPOOL_CAPACITY | 1000 | In-memory usage queue capacity |
Transaction traffic never falls back to ARETE_READ_RPC_URL. Use a dedicated provider credential when possible so read and transaction quotas are isolated.
Submission safety
Section titled “Submission safety”The send route:
- accepts only bounded base64 wire transactions
- requires nonzero signatures
- derives the first transaction signature before contacting upstream, from the leading shortvec for legacy and v0 and from the tail signature array for v1 (SIMD-0385, version byte
129) - calls upstream
sendTransactiononce - leaves provider
maxRetriesunset, allowing the upstream RPC to rebroadcast until the blockhash expires - verifies that the provider returns the expected signature
- marks timeout/transport uncertainty with the derived signature
Clients must reconcile an ambiguous signature rather than resubmit it blindly.
Limits and scaling
Section titled “Limits and scaling”The server clamps request size, concurrency, and per-minute operation counts. Signed-session claims can impose lower limits per subject. Rate buckets and subject concurrency are process-local in V1.
Keep a transaction-enabled deployment at one replica until admission state is externalized. Scaling replicas multiplies effective per-process limits.
Usage and observability
Section titled “Usage and observability”Optional transaction usage delivery sends operation, result, identity, byte count, and latency metadata. It excludes transaction bytes, account lists, program logs, credentials, and full signatures.
V1 uses a bounded in-memory queue with bounded retries. It is not a durable billing queue. Alert on queue saturation and failed delivery logs.
With the otel feature, monitor:
arete.transaction.requests.totalarete.transaction.request.durationarete.transaction.request.bytesarete.transaction.upstream.totalarete.transaction.inflightarete.transaction.denials.total
Operational checklist
Section titled “Operational checklist”- Use a dedicated HTTPS transaction RPC.
- Configure signed-session HTTP auth and exact scopes.
- Configure trusted proxy CIDRs correctly.
- Keep one replica in V1.
- Alert on upstream timeouts and ambiguous submissions.
- Alert on usage queue drops when usage tracking is enabled.
- Exercise a signed test transaction before enabling browser traffic.
- Preserve routes during rollback until issued transaction tokens expire.
Improvements still needed
Section titled “Improvements still needed”The most valuable follow-up work is:
- Durable, batched usage delivery with a dead-letter path.
- Shared rate/admission state for horizontal scaling.
- Configurable circuit breaking and provider failover for inspect/status calls only.
- A reusable transport conformance test suite.
- Validator-backed release tests and hosted browser canaries.
- Grafana dashboards and alerts for transaction-specific metrics.
See Using transactions for application integration and error handling.