Hkchain

Accounts and keys

An Hkchain account is an ordinary Cosmos account with an Ethereum-compatible key. What is specific to this chain is what sits beside the account: an identity record that the compliance gates read, and optionally a delegation that lets a second key spend under a policy.

Key type

Keys are eth_secp256k1 — secp256k1 with Ethereum's key and address derivation. That is the same curve and the same derivation MetaMask uses, which is what makes one key usable on both paths.

The practical consequences:

  • A key generated by Ethereum tooling works here without conversion.
  • The node's own key commands default to this algorithm, so keys created with hkchaind keys add <name> are EVM-compatible by construction.
  • Signatures are produced over Cosmos sign bytes on the Cosmos path and over RLP-encoded Ethereum transactions on the EVM path. Same key, two envelopes.

One account, two address encodings

Every account has a Bech32 form and a hex form. They are the same 20 bytes:

FormLooks likeUsed by
Bech32hkchain1…Cosmos messages, REST paths, identity records, Travel Rule payloads
Hex0x…EVM calls, MetaMask, ERC-20 arguments, EVM event topics

Nothing on the chain treats them as different accounts — a balance credited to the hex form is visible at the Bech32 form and vice versa. But your systems will treat them as different strings unless you normalise, so pick one canonical form at your boundary and convert on the way in.

Two places where the distinction leaks into query syntax:

  • Identity, sub-role and reserve queries take the Bech32 form in the path.
  • Transaction search keys on message.sender in both cases, but an EVM transaction emits that attribute twice — once with the hex form of the Ethereum sender and once with the Bech32 form of the debited account. Searching the Bech32 form therefore covers stablecoin movement from either path; searching the hex form narrows to the EVM ones. See Broadcasting and confirmation for the query syntax.

The repository carries a small eth2bech32 converter for the manual case.

Account number and sequence

Signing requires both, and both come from the standard auth query:

curl -s "$REST/cosmos/auth/v1beta1/accounts/hkchain1..."

Read the response with one caveat: an EVM-compatible account is returned wrapped, so the fields may be at the top level or nested under base_account. Handle both shapes — the wrapping depends on how the account was created, not on anything you control.

A 404 means the account has never appeared on chain. It has no account number yet, so it cannot sign anything; fund it first and the chain creates it.

sequence increments once per accepted transaction from that account. Two transactions signed at the same sequence conflict — the second is rejected. If you submit in parallel, track the sequence yourself rather than re-reading it between sends, because the query reflects committed state and lags what you have already broadcast.

Identity: the record beside the account

Holding a key is not permission to move stablecoin. The chain keeps a separate registry keyed by address:

curl -s "$REST/hkchain/kyc/v1/identity/hkchain1..."

The record carries a level, the VASP the account is bound to, and a reference to identity records held off chain. It carries no personal data — the reference is a hash, and the records themselves stay with the institution that performed the checks.

Levels, and the Travel Rule tier each one is registered with:

LevelNameRegistered tierWhat it means for an integrator
0LEVEL_BLACKLISTEDnoneEvery stablecoin interaction is rejected, in either direction
1LEVEL_UNVERIFIEDnoneDefault on a test network; may receive, cannot originate a transfer
2LEVEL_RETAIL_BASICminimalMay send below the enhanced-data threshold
3LEVEL_RETAIL_ENHANCEDfullMay send at any amount
4LEVEL_INSTITUTIONALfullLicensed VASPs and institutional clients; no amount cap
5LEVEL_LICENSED_ISSUERfullMay additionally mint and burn its own denom

The two ends of the table are enforced by different gates, which is worth knowing when you read a rejection. Blacklisting is checked first and directly, for both parties. Everything above it is enforced through the tier: the level determines which Travel Rule tier the account may claim, a transfer whose claimed tier does not match the registered one is rejected, and a tier of none cannot satisfy any amount — which is why an unverified account cannot send even though nothing explicitly says "unverified may not send".

The level definitions are module parameters, not constants. Read the live set from $REST/hkchain/kyc/v1/params rather than assuming these defaults on an unfamiliar network. Building and signing covers the tier interaction; the compliance model covers the reasoning.

A level is never something an application sets. It is written by whoever the network designates as its identity provider, off the transaction hot path.

Native transfers are outside all of this

An ahkc transfer carries no identity requirement, passes no gate, and needs no payload — including from an address with no identity record at all. The regulated perimeter is the stablecoin, not the chain.

This is deliberate and it is load-bearing for onboarding: an address can be funded with gas before it has any identity, which is the only way the first identity transaction can be paid for.

Delegating to a second key

An account can bind a sub-key that signs on its behalf. Three sub-roles exist, each with the message set it is intended for:

Sub-roleIntended to signAdditional enforcement
AgentMsgSend onlyA policy envelope: per-transaction cap, 24-hour spend cap, and a revoked flag
ReviewerMsgReviewFlag, MsgEscalateFlag—
OfficerMsgCloseFlag—

Read that table as the contract a delegated key is built to, not as a complete fence. Two facts sit behind it. The sets are fixed in the chain software, so binding a sub-role selects one rather than defining it. And the gate enforces a set over the message types it can extract a signer from — MsgSend plus the compliance administrative messages, the scope described under "What the gates recognise, and what they do not" on Transaction lifecycle — so a message outside that scope falls through unchecked rather than being rejected.

The practical consequence is the same either way: construct a delegated key's transactions inside its row of the table, and keep your own controls on what that key is allowed to build. Do not treat "the chain would have rejected it" as one of those controls.

Three properties matter when you integrate against a delegated key:

  • The sub-key inherits the parent's identity, for identity checks. It has no identity record of its own, and the gates resolve the signer to its parent before any identity lookup — so an agent transfer is evaluated against the parent's level, VASP and references, and the Travel Rule payload must describe the parent, not the agent.
  • Attribution is not inherited. Compliance flags and audit entries record the key that signed, not the parent. So does the velocity count, which is kept per key. If you present activity by account, resolve the parent yourself — the chain will not have done it for you in these records.
  • Revocation takes effect at the next block. There is no grace period and no in-flight exemption.

How the 24-hour cap actually resets

The agent spend window is a fixed period, not a sliding one. The first stablecoin spend after a reset starts a 24-hour period; every spend inside it accumulates against daily_cap; the first spend more than 24 hours after that start begins a fresh period at zero.

The practical difference from a sliding window: capacity does not trickle back as old spends age out. An agent that exhausts its cap early in a period stays at zero until the period turns over, and the turnover is anchored to when it first spent — not to midnight, and not to a fixed offset you can compute without reading the window. A rejection message here says rolling-24h spend; read that as the period's accumulated total.

Read the position rather than modelling it:

curl -s "$REST/hkchain/compliance/v1/agent_policy/hkchain1<parent>/hkchain1<sub>"

The response carries the envelope under policy and the counter under window: window_start is the anchor and spent_last_24h is what has accumulated since. One caveat when you display it — the reset is applied on the next spend, not on read. If window_start is already more than 24 hours old, spent_last_24h is the previous period's total and the effective available capacity is the full cap. Compare window_start to the current time before showing a remaining balance.

To discover the relationship in either direction:

# What has this account delegated?
curl -s "$REST/hkchain/compliance/v1/sub_roles/hkchain1<parent>"

# Whose key is this?
curl -s "$REST/hkchain/compliance/v1/sub_role_parent/hkchain1<sub>"

# What is the agent's spending envelope?
curl -s "$REST/hkchain/compliance/v1/agent_policy/hkchain1<parent>/hkchain1<sub>"

An integration that shows account activity should resolve the parent before presenting a transfer as "from" an address — otherwise a delegated payment looks like it came from an unknown account with no identity.

Where to go next