Issuer service (arrekta-issuer)
Runs issued assets after issuance: investor registry, transfer restrictions, allocations and distributions, governed controls (freeze, clawback, rule changes), dividends, reserve attestations, and a public transparency log.
On Arrekta, compliance is enforced by co-signature and by consensus controls, never by contract code:
- Restricted assets live only in managed addresses: 2-of-2 multisig locks over the
holder's key and the asset's authorizer key.
- The authorizer co-signs a transfer only when it passes the rules. This is the model Blockstream AMP uses, built here from standard Arrekta locks and PSTs.
- Holders can't move the asset without the authorizer. The authorizer can't move it without the holder.
- Compromising the authorizer key loses enforcement, not funds.
- Freeze and clawback are consensus rules for assets issued with those flags (§7.3–7.4). The service decides when they may be used (officer quorum, time lock, reason code, evidence hash) and makes every use public.
Clients reach it through the gateway at /issuer/v1/* with an API key. GET /issuer/v1/public/* needs no key.
Setup
- Treasury. Create a wallet in the wallet service to hold the issuer's supply and the asset's control token. Issue the asset on chain with its supply and control token paid to a treasury address.
- Issuer.
POST /issuers:The caller becomes owner and first officer, and must own or co-sign the treasury wallet.{"name": "Example Capital", "officers": ["key_b"], "quorum": 2, "control_delay_secs": 86400, "treasury_wallet": "wal_…"} - Trusted claim issuers.
POST /issuers/{id}/claim-issuers {"name", "public_key"}, owner only. Each claim issuer is an ML-DSA key, for example a KYC provider's. - Asset.
POST /issuers/{id}/assets:{"asset": "<hex or ticker>", "mode": "restricted", "rules": {…}, "metadata": {…ARRK-AMS-1…}}- Only the coin's issuer can register it, because wallets show registered coins as
verified: the issuance transaction must have been paid from the issuer's treasury wallet,
or the treasury must hold the coin's authority (mint) or control token
(
403 forbiddenotherwise). - The metadata document must hash to the asset's on-chain
metadata_hash(422 metadata-mismatchotherwise). - The response reports
controls_ready: whether the control token is in the treasury. - Registration writes the first entry of the asset's public log, including its authorizer public key.
- Only the coin's issuer can register it, because wallets show registered coins as
verified: the issuance transaction must have been paid from the issuer's treasury wallet,
or the treasury must hold the coin's authority (mint) or control token
(
Investors and claims
- Investors.
POST /issuers/{id}/investors {"name", "external_ref"}. - Claims.
POST /investors/{inv}/claims {"type", "value", "expires_at", "claim_issuer", "signature"}. The claim issuer signs, with context"ARRK-ISSUER-CLAIM-v1":(TH("issuer-claim", varstr issuer_id ‖ varstr investor_id ‖ varstr type ‖ varstr value ‖ u64 expires_at)expires_atis in unix seconds; 0 means never.) An unsigned or forged claim gets403 bad-claim-signature. - Valid claims are unrevoked, unexpired, and issued by an active trusted claim issuer.
DELETE /claims/{id}revokes a claim. - Holder addresses.
POST /assets/{asset}/holders:- Restricted assets take
{"investor_id", "public_key", "signature"}. The holder proves control by signing, with context"ARRK-ISSUER-HOLDER-v1":The response is the managed address and its redeem policy (TH("issuer-holder", varstr asset_hex ‖ varstr investor_id ‖ varbytes public_key)[holder, authorizer], m = 2). - Open assets take
{"investor_id", "address"}.
- Restricted assets take
- Cap table.
GET /assets/{asset}/holdersreturns live balances per investor and address.
Rules
{"receiver": [[{"claim": "kyc"}, {"jurisdiction_not_in": ["KP", "IR"]}],
[{"claim": "kyc"}, {"claim": "accredited"}]],
"sender": [[{"claim": "kyc"}]],
"max_holders": 2000, "max_balance": "1000000", "lockup_until_height": 120000}- Groups: a side passes when every condition in at least one group holds. No groups means any registered investor passes.
- Conditions:
{"claim": type},{"jurisdiction_in": [...]},{"jurisdiction_not_in": [...]}. Codes are ISO 3166-1 alpha-2. An investor without a jurisdiction claim never passes a jurisdiction condition. - Limits:
max_holderscaps the number of investors holding a positive balance.max_balancecaps each holder's balance after the transfer.lockup_until_heightblocks holder-to-holder transfers until that height; issuer distributions are not affected.
- Changing rules is a governed action (see Controls), and the change is public.
- Dry run:
POST /assets/{asset}/check {"to_investor", "amount", "from_investor"}returns{allowed, reasons}.
Holder transfers (restricted assets)
-
The holder builds a PST spending its managed address, with
redeem= its policy, and signs its slot. -
POST /assets/{asset}/authorize {"pst": "<base64>"}, with any API key (for example the holder's app). -
The authorizer checks the PST:
- every non-clawback input holding the asset comes from a managed address;
- every output of the asset goes to an active managed address, or back to an address of the issuer's own treasury wallet (a redemption: receiver rules don't apply to the issuer);
- all asset inputs belong to one investor;
- the rules hold for the sender and every recipient.
PSTs that move only ARRK out of a managed address are approved; holders may need ARRK there for fees.
-
If approved, the response carries the PST with the authorizer's signature. The holder finalizes it and broadcasts. Refusals return
403 transfer-not-compliantwith the reasons. Every decision is recorded.
Allocations and distributions
- Allocate.
POST /assets/{asset}/assignments {"investor_id", "amount", "vesting_height"}. The receiver must already be eligible (422 not-eligible). - Distribute.
POST /assets/{asset}/distributionspays every pending, vested assignment to the investor's latest active address. Payments go out in treasury-wallet transfers of up to 100 recipients. The treasury's co-signers sign them in the wallet service, whose policy also applies. - Settlement. A worker follows the wallet transfers: confirmed assignments become
distributed, and failed ones return topending. A finished distribution is written to the public log.
Controls: freeze, unfreeze, clawback, rules
- Propose. An officer proposes:
{"kind": "clawback", "address": "…", "destination": "…", "reason_code": "court_order", "reference_hash": "<32-byte hex>", "note": "…"}- Reason codes:
court_order,sanctions,fraud,lost_key_recovery,regulatory,error_correction,other(needs a note). reference_hashis the hash of the evidence, such as a court order.
- Reason codes:
- Approve. Other officers approve (
POST /actions/{id}/approve) until the issuer's quorum is met.- Rule changes apply immediately.
- Freezes and unfreezes become executable.
- Clawbacks become executable after
control_delay_secs; until then execution returns409 timelocked.
- Execute.
POST /actions/{id}/executebuilds a transaction that spends the control token and creates it again (so it never leaves the treasury). It carries the freeze record, or the clawed inputs plus an output to the destination. The transaction is imported into the treasury wallet as a PST, where the treasury's co-signers and policy apply. - Log. Once confirmed, the action is
executedand appears in the public log with its reason code, evidence hash, approval count and txid.
Lost-key recovery is a clawback from the old managed address to the investor's new one.
Dividends
- Declare.
POST /assets/{asset}/dividends {"record_height", "payout_asset": "ARRK", "total_amount"}.- The record height must be at least
ARREKTA_ISSUER_RECORD_CONFIRMATIONS(default 6) blocks deep, and the indexer must have reached it. - Treasury addresses are excluded. Restricted assets count only registered addresses.
- The record height must be at least
- Allocation: largest-remainder method. Payouts sum to exactly the total, and ties go to the lowest address.
- Signed table. The entitlement table is hashed and signed with the log key (context
"ARRK-ISSUER-DIVIDEND-v1"), so any holder can check its line:TH("dividend", varstr asset ‖ u32 height ‖ varstr payout_asset ‖ u64 total ‖ varint n ‖ n × [varstr address ‖ u64 balance ‖ u64 amount]) - Pay.
POST /dividends/{id}/paypushes the payments through the treasury wallet; the state goespaying→paid.
Reserve attestations
POST /assets/{asset}/attestations:
{"as_of", "total_reserves", "currency", "reserves": [...], "auditor", "report_url", "document_hash"}The live circulating supply and chain height are recorded next to the report, and the entry goes into the public log.
Public transparency
| Endpoint (no API key) | Content |
|---|---|
GET /public/assets | Registered assets |
GET /public/assets/{asset} | Mode, rules, authorizer key, controls (officers, quorum, clawback delay, executed actions), supply, metadata hash, latest attestation, log head |
GET /public/assets/{asset}/metadata | The verified ARRK-AMS-1 document |
GET /public/assets/{asset}/log | The signed hash chain |
GET /public/keys | The log key |
Each log entry's hash is computed as follows:
hash = TH("issuer-log", prev_hash(32) ‖ u64 seq ‖ varstr kind ‖ varstr canonical_json(data) ‖ u64 at)
It is signed with context "ARRK-ISSUER-LOG-v1". A deleted or edited entry breaks the chain for
anyone holding an earlier copy. all_svc/issuer_check.py verifies it independently in Python
with OpenSSL.
Operations
- Image and compose: image
arrekta/arrekta-issuer:local(arrekta-compose.sh build issuer), compose servicearrekta-issuer(profileplatform, internal port 3006). - Database:
ISSUER_DB_*. - Settings:
ARREKTA_ISSUER_SIGNING_SEED: signs the log, dividend tables and attestations.ARREKTA_ISSUER_AUTHORIZER_SEED: root of the authorizer keys. Back it up. Without it, restricted assets can't move until they are clawed back to new addresses.ARREKTA_ISSUER_RECORD_CONFIRMATIONSandARREKTA_SERVICE_TOKEN.
- Checks:
core/.venv/bin/python all_svc/issuer_check.py(42 end-to-end checks).