Api

General API

Application-level operations

The General API exposes Hydra App's AppService — application-level operations including network discovery, public key retrieval, payment preimage lookup, archive retention, and invite / referral management.

JSON-RPC namespace: app

Endpoints


The Network type

Most other APIs accept a Network to identify a specific blockchain. It has just two fields:

FieldTypeDescription
protocolProtocol enumPROTOCOL_BITCOIN (1), PROTOCOL_EVM (2), or PROTOCOL_TRON (3)
idstringNetwork identifier — magic bytes (hex) for Bitcoin, decimal chain ID for EVM and Tron. Staging values: "0a03cf40" (Bitcoin Signet), "11155111" (Ethereum Sepolia), "421614" (Arbitrum Sepolia), "2494104990" (Tron Shasta).

See the network identifier table in the Setup Guide for every supported network.


Get Networks

Returns the list of blockchain networks that have been initialized and are currently active in this application instance.

Method: GetNetworks

Parameters: None

Response:

FieldTypeDescription
networksNetwork[]Active networks

Example Request:

import { AppServiceClient } from './proto/AppServiceClientPb'
import { GetNetworksRequest } from './proto/app_pb'

const client = new AppServiceClient('http://localhost:5003')

const response = await client.getNetworks(new GetNetworksRequest(), {})
response.getNetworksList().forEach(n => {
  console.log(`Protocol ${n.getProtocol()} / id ${n.getId()}`)
})

Example Response:

{
  "networks": [
    { "protocol": 1, "id": "0a03cf40" },
    { "protocol": 2, "id": "11155111" },
    { "protocol": 2, "id": "421614" }
  ]
}

Get Public Key

Returns the Ed25519 public key of this application instance. The key uniquely identifies the application and is derived from the user's mnemonic seed. It does not depend on a network.

Method: GetPublicKey

Parameters: None

Response:

FieldTypeDescription
public_keybytesEd25519 public key (32 bytes)

Example Request:

import { GetPublicKeyRequest } from './proto/app_pb'

const response = await client.getPublicKey(new GetPublicKeyRequest(), {})
const pubkey = response.getPublicKey_asU8()
console.log('Public key (hex):', Buffer.from(pubkey).toString('hex'))

Example Response:

{
  "public_key": "AbCdEf0123...="
}

The bytes field is base64 in JSON-RPC; in gRPC it's a raw bytes.


Get Preimage

Looks up a payment preimage by its hash. Returns the preimage if it has been revealed or registered, or empty if the preimage is not yet known.

Method: GetPreimage

Parameters:

NameTypeRequiredDescription
payment_hashbytesYESPayment hash to look up (32 bytes)

Response:

FieldTypeDescription
payment_preimagebytes (optional)The preimage (32 bytes), or empty if not yet known

Example Request:

import { GetPreimageRequest } from './proto/app_pb'

const request = new GetPreimageRequest()
request.setPaymentHash(Buffer.from('a1b2c3...32bytes...', 'hex'))

const response = await client.getPreimage(request, {})
const preimage = response.getPaymentPreimage_asU8()
if (preimage && preimage.length > 0) {
  console.log('Preimage:', Buffer.from(preimage).toString('hex'))
} else {
  console.log('Preimage not yet known')
}

Example Response:

{ "payment_preimage": "9f8e7d6c..." }

Archive Prune

Redesigned 2026-07-08 — was the synchronous PruneArchive (added 2026-05-26).

Operator-driven retention for archive-side settled history — settled wallet transactions and settled payments — bounded by max age and/or max count. Pending entries are never pruned. As of 2026-07-08 pruning is an asynchronous job, not a single blocking call: you start a job, then poll its status or subscribe to its events. At most one prune job runs per network.

Breaking (2026-07-08): the old PruneArchive RPC (with total_pruned / per_table) was removed. Migrate to StartArchivePrune + GetArchivePruneStatus (or SubscribeArchivePruneEvents).

Pruning a settled payment also deletes its stored preimage — a pruned payment's preimage is no longer served by GetPreimage.

Start Archive Prune

Method: StartArchivePrune

NameTypeRequiredDescription
networkNetworkYESNetwork whose archive to prune (Bitcoin and EVM networks each have their own archive instance)
max_age_secsuint64NO*Keep only entries younger than this many seconds (from server now)
max_itemsuint64NO*Keep at most this many newest entries per archive table (≥ 1)

*At least one filter must be set. Filters compose intersectively — an entry is kept only if it satisfies every set filter.

Response: job (ArchivePruneJob) + newly_started (bool). When newly_started is false, a job was already running and its (possibly different) descriptor is returned instead of starting a new one.

Get Archive Prune Status

Method: GetArchivePruneStatus — parameters: network (Network, YES). The authoritative state.

Response: running (RunningArchivePrune, optional — the live job + an ArchivePruneProgress snapshot + cancel_requested) and last (ArchivePruneRecord, optional — the most recently finished job since process start, with a completed / failed / cancelled outcome). Both unset ⇒ no prune has run since start.

Cancel Archive Prune

Method: CancelArchivePrune — parameters: network (Network, YES). Requests cancellation; the job stops at its next chunk boundary and finishes with a cancelled outcome. Returns cancelled = false when no job is running. Idempotent.

Subscribe Archive Prune Events

Method: SubscribeArchivePruneEvents (server-streaming) — no parameters; the stream covers all networks. Each ArchivePruneEvent carries its ArchivePruneJob and one of started / progress / completed / failed / cancelled / retention_lagging. Delivery is best-effort — on stream end, re-subscribe and reconcile with GetArchivePruneStatus.

retention_lagging (2026-08-26) carries ArchivePruneRetentionLagging { lag_secs: uint64 } — how far past its retention deadline the worst archive phase is, in seconds. The archive is not draining fast enough for the configured policy, so the node is escalating how hard it prunes — at the cost of foreground latency — until the backlog clears. Repeated periodically while it persists.

This is informational, not a failure: the job is still running and still making progress. It is the signal to widen max_age_secs / max_entries, give the node more I/O, or accept the added latency — a controller that only ever backs off fills the disk instead. Add a branch for it only if you surface prune progress.

Example — start and poll:

import { StartArchivePruneRequest, GetArchivePruneStatusRequest } from './proto/app_pb'

const start = new StartArchivePruneRequest()
start.setNetwork({ protocol: 1, id: '0a03cf40' })
start.setMaxAgeSecs(60 * 60 * 24 * 90)   // keep 90 days
start.setMaxItems(100_000)               // and at most 100k newest per table

const { job } = (await client.startArchivePrune(start, {})).toObject()
console.log(`prune job ${job.jobId} started`)

// Poll until the running job clears.
const statusReq = new GetArchivePruneStatusRequest()
statusReq.setNetwork({ protocol: 1, id: '0a03cf40' })
for (;;) {
  const status = (await client.getArchivePruneStatus(statusReq, {})).toObject()
  if (!status.running) {
    const last = status.last
    console.log('done:', last?.counts, last?.completed ? 'completed' : last?.failed ? 'failed' : 'cancelled')
    break
  }
  console.log(`  phase ${status.running.progress?.phase}, ${status.running.progress?.chunks} chunks`)
  await new Promise(r => setTimeout(r, 1000))
}

Prefer configuring settings.auto_prune in config.yaml (periodic auto-prune per network) over calling this by hand — see the Setup Guide. The RPCs are for on-demand / operator-driven runs.


Create Invite

Invites are how mainnet admission works at launch. Redeeming one is what gets an identity admitted, not something you do once you are already in — see Mainnet access is gated.

Added 2026-07-08. Requires referral_config.referral_service_url in config.yaml.

Mints a fresh bearer invite code to share with another user out-of-band. The application signs the server-issued challenge with its identity key internally — no parameters are required.

Method: CreateInviteParameters: None.

Response: code (string) — the bearer invite code (URL-safe base64). Anyone holding it can redeem it, so share it privately.


Redeem Invite

Added 2026-07-08.

Redeems an invite code received from another user. The application signs the redemption internally.

Method: RedeemInvite

NameTypeRequiredDescription
codestringYESThe bearer invite code received from an inviter

Response: referral_public_key (bytes, 32) — the inviter's Ed25519 public key, so the UI can surface "you were invited by X".


List Invites

Added 2026-08-19. Requires referral_config.referral_service_url in config.yaml.

Lists the invite codes this application identity has minted, newest first, each with its current status.

Method: ListInvites

NameTypeRequiredDescription
status_filterInviteStatusFilternoNarrows to one status; defaults to unfiltered

InviteStatusFilter: INVITE_STATUS_FILTER_UNSPECIFIED (0, everything), ..._PENDING (1), ..._REDEEMED (2), ..._EXPIRED (3).

Response: { invites: Invite[] } — empty when the identity has never minted a code, or when none match the filter.

Invite:

FieldTypeDescription
codestringThe bearer code, as returned by CreateInvite
created_epochint32The epoch the code was minted in
created_atTimestampWhen it was minted
expires_atTimestamp?When it stops being redeemable. Unset = never expires
statusoneofExactly one of pending / redeemed / expired is always set

InvitePending and InviteExpired are empty markers. InviteRedeemed carries redeemed_by_public_key (bytes, 32 — the invitee this code onboarded) and redeemed_at (Timestamp), so a redeemed invite structurally carries its redeemer — no second lookup.

⚠️ A pending code is still a bearer secret

Anyone holding an unredeemed code can redeem it. The application proves ownership of its identity key to the referral service before this list is returned, precisely so a code is only ever shown to its creator. Treat the response like credentials: don't log it, don't render a pending code anywhere it can be shoulder-surfed or screenshotted into a support ticket.


Get Invite Eligibility

Added 2026-08-19.

This application identity's standing in the invite program — enough to render quota usage and mint-gating without a doomed mint round-trip.

Method: GetInviteEligibilityParameters: None.

Response:

FieldTypeDescription
eligibilityInviteEligibility?Unset = this identity cannot mint invites at all (it can still redeem one)
current_epochint32The referral service's current epoch (1-based) — the same value the mint gates evaluate

InviteEligibility:

FieldTypeDescription
joined_epochint32The epoch this identity joined the program
is_seedbooltrue for identities provisioned by the operator rather than onboarded through an invite
invite_quotaint32Lifetime cap on codes this identity may mint
invites_mintedint32Codes minted so far
created_atTimestampWhen the eligibility row was created

Minting is allowed when both hold:

current_epoch > joined_epoch        // you cannot invite in the epoch you joined
invites_minted < invite_quota

Quota is spent at mint, not at redeem. An expired or never-redeemed code still counts against invite_quota — minting codes speculatively burns the allowance permanently.

Unlike ListInvites, this call is unauthenticated on the referral service side — quotas and epochs are not bearer secrets — so there is no signature round-trip and it is cheap to poll for a UI.


Get Referral

Added 2026-07-08.

Returns the application's current referrer, if one has been set (e.g. via RedeemInvite).

Method: GetReferralParameters: None.

Response: referral_public_key (bytes, optional) — the referrer's Ed25519 public key, or absent if none is set.


Common Workflows

Initialize and discover networks

async function initializeApp(client: AppServiceClient) {
  const networks = (await client.getNetworks(new GetNetworksRequest(), {})).getNetworksList()
  const pubkey = (await client.getPublicKey(new GetPublicKeyRequest(), {})).getPublicKey_asU8()

  return {
    publicKey: Buffer.from(pubkey).toString('hex'),
    networks: networks.map(n => ({ protocol: n.getProtocol(), id: n.getId() }))
  }
}

Protocol Types

ValueConstantDescription
0PROTOCOL_UNSPECIFIEDInvalid default — never use
1PROTOCOL_BITCOINBitcoin mainnet, testnet, signet, regtest
2PROTOCOL_EVMEthereum, Arbitrum, Polygon, BSC, etc.
3PROTOCOL_TRONTron mainnet, Shasta, Nile (TVM)

Common Network IDs

Bitcoin (PROTOCOL_BITCOIN, magic bytes hex)

idNetwork
f9beb4d9Bitcoin Mainnet
0b110907Bitcoin Testnet3
0a03cf40Bitcoin Signet
fabfb5daBitcoin Regtest

EVM (PROTOCOL_EVM, decimal chain ID)

idNetwork
1Ethereum Mainnet
42161Arbitrum One
11155111Ethereum Sepolia
421614Arbitrum Sepolia
137Polygon
10Optimism

Tron (PROTOCOL_TRON, decimal chain ID)

idNetwork
728126428Tron Mainnet
2494104990Tron Shasta
3448148188Tron Nile

Best Practices

  1. Cache the network list. Active networks rarely change; refresh on app start.
  2. Treat the public key as stable. It's derived from the seed and won't change for the application's lifetime.
  3. Check GetPreimage results for emptiness. A successful response can still mean "not yet known" — it's not an error.
  4. Prefer config-driven auto_prune over calling StartArchivePrune by hand. Set settings.auto_prune in config.yaml for periodic retention; use the RPCs for on-demand runs, and poll GetArchivePruneStatus (or subscribe) rather than assuming a start call finished.

← Back to API Reference | Next: Wallet API →


Copyright © 2025