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:
| Form | Looks like | Used by |
|---|---|---|
| Bech32 | hkchain1… | Cosmos messages, REST paths, identity records, Travel Rule payloads |
| Hex | 0x… | 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.senderin 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:
| Level | Name | Registered tier | What it means for an integrator |
|---|---|---|---|
| 0 | LEVEL_BLACKLISTED | none | Every stablecoin interaction is rejected, in either direction |
| 1 | LEVEL_UNVERIFIED | none | Default on a test network; may receive, cannot originate a transfer |
| 2 | LEVEL_RETAIL_BASIC | minimal | May send below the enhanced-data threshold |
| 3 | LEVEL_RETAIL_ENHANCED | full | May send at any amount |
| 4 | LEVEL_INSTITUTIONAL | full | Licensed VASPs and institutional clients; no amount cap |
| 5 | LEVEL_LICENSED_ISSUER | full | May 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-role | Intended to sign | Additional enforcement |
|---|---|---|
| Agent | MsgSend only | A policy envelope: per-transaction cap, 24-hour spend cap, and a revoked flag |
| Reviewer | MsgReviewFlag, MsgEscalateFlag | — |
| Officer | MsgCloseFlag | — |
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
- Transaction lifecycle — where these records are consulted, and in what order.
- Building and signing — turning an account into a signed transaction.