Issuer platform from the SDK
The Arrekta platform services sit behind the gateway, and both SDKs cover them with the same functions (camelCase in TypeScript, snake_case in Python):
| Service | Path | What it does |
|---|---|---|
| Gateway | /gateway/v1/* | API keys (each key's id is your principal), usage, signed webhooks |
| Wallet service | /wallet/v1/* | m-of-n wallets whose co-signers keep their own keys; policy engine; PST signature collection |
| Issuer service | /issuer/v1/* | investor registry and signed claims; transfer rules by co-signature; airdrops, dividends, governed controls, attestations, public signed log |
| Indexer | /esplora/* | asset supply history and cap tables at any height |
| Faucet | /faucet/v1/* | test ARRK behind a proof-of-work captcha |
Arrekta Studio (apps/studio) is built only on these calls; read its code for a complete
application. Design: Studio.
1. A 2-of-3 treasury
Each co-signer derives keys on its own device and registers them in a batch signed by its identity key, so the service cannot substitute keys.
import { createGatewayClient, createServiceWallet, cosignerIdentityHex, registerWalletKeys, walletKeyBatch, nextWalletAddress } from 'arrekta-sdk'
const gw = createGatewayClient({ url: 'http://127.0.0.1:3000', apiKey: 'arrk_…' })
const w = await createServiceWallet(gw, {
label: 'treasury', m: 2,
cosigners: [
{ name: 'issuer', identity_key: cosignerIdentityHex(deviceSeed), principal: myKeyId },
{ name: 'co-signer', identity_key: cosignerIdentityHex(cosignerSeed) },
{ name: 'backup', identity_key: cosignerIdentityHex(backupSeed) },
],
})
await registerWalletKeys(gw, { wallet: w.id, batch: walletKeyBatch({ root: deviceSeed, walletId: w.id, cosigner: 0 }) })
// … the other co-signers register theirs; addresses appear once all three have
const { address } = await nextWalletAddress(gw, { wallet: w.id })from arrekta_sdk import GatewayClient, cosigner_identity_hex, wallet_key_batch
gw = GatewayClient("http://127.0.0.1:3000", api_key="arrk_…")
w = gw.create_service_wallet("treasury", [
{"name": "issuer", "identity_key": cosigner_identity_hex(device_seed), "principal": my_key_id},
{"name": "co-signer", "identity_key": cosigner_identity_hex(cosigner_seed)},
{"name": "backup", "identity_key": cosigner_identity_hex(backup_seed)},
], m=2)
gw.register_wallet_keys(w["id"], wallet_key_batch(device_seed, w["id"], 0))2. Issue a coin from the treasury
Build an unsigned PST from the wallet's coins and import it: the wallet service applies the
policy, then each device signs. The coin and its authority and control tokens stay in the
treasury. Fees assume 2-of-3 inputs (multisigInputSize).
import { buildServiceWalletPst, createIssuance, createWalletTransfer, getWalletUnspents, issuedAssetId, metadataHash,
pstFromBase64, pstSummary, pstToBase64, signPstWithSeed, submitWalletSignatures, verifyPstAttestation, getWalletAttestationKey, AssetFlags } from 'arrekta-sdk'
const doc = { schema: 'arrk-ams-1', ticker: 'BOND', name: 'Green Bond', issuer: { name: 'Example Capital' }, controls_policy: '…' }
const issuance = createIssuance({ ticker: 'BOND', name: 'Green Bond', amount: 1_000_000n, flags: AssetFlags.FREEZABLE | AssetFlags.CLAWBACK, metadataHash: metadataHash(doc) })
const unspents = await getWalletUnspents(gw, { wallet: w.id })
const { pst, tx } = buildServiceWalletPst({ unspents, network: 'regtest', payments: [], change: address, feeRate: 2000, issuances: [issuance] })
const t = await createWalletTransfer(gw, { wallet: w.id, sequenceId: 'issue-bond', pst })
const key = await getWalletAttestationKey(gw)
if (!verifyPstAttestation(t.pst!, t.attestation!.signature, key.public_key)) throw new Error('tampered PST')
console.log(pstSummary(pstFromBase64(t.pst!), deviceSeed)) // what leaves, change, fee
await submitWalletSignatures(gw, { transfer: t.id, pst: pstToBase64(signPstWithSeed(pstFromBase64(t.pst!), deviceSeed)) })Python: build_service_wallet_pst(unspents, network=…, change=…, fee_per_kb=…, issuances=[…]),
then create_wallet_transfer, verify_pst_attestation, submit_wallet_signatures.
3. Investors, claims and holders
import { createIssuer, addClaimIssuer, createInvestor, addInvestorClaim, signIssuerClaim, registerIssuerAsset, registerHolder, holderProof } from 'arrekta-sdk'
const issuer = await createIssuer(gw, { name: 'Example Capital', treasuryWallet: w.id, quorum: 1 })
await registerIssuerAsset(gw, { issuer: issuer.id, asset: assetHex, mode: 'restricted', rules: { receiver: [[{ claim: 'kyc' }]] }, metadata: doc })
const ci = await addClaimIssuer(gw, { issuer: issuer.id, name: 'KYC', publicKey: kycKeyHex })
const alice = await createInvestor(gw, { issuer: issuer.id, name: 'Alice' })
await addInvestorClaim(gw, { investor: alice.id, type: 'kyc', value: 'true', claimIssuer: ci.id,
signature: signIssuerClaim(kycKey, { issuerId: issuer.id, investorId: alice.id, type: 'kyc', value: 'true' }) })
// the holder proves control of their key; the result is a managed 2-of-2 [holder, authorizer] address
const managed = await registerHolder(gw, { asset: assetHex, holder: holderProof(aliceKey, assetHex, alice.id) })A holder moves a restricted coin by signing a PST from the managed address and asking
authorizeTransfer for the authorizer's co-signature. The rules are checked first.
Show holders their address under your coin's own ticker: encodeCoinAddress(lock, ticker, network)
(encode_coin_address) turns rarrk1… into <ticker>-r1… (<ticker>1… on mainnet), the same lock.
Studio portals do this for every coin.
4. Airdrops, rewards and controls
| Task | TypeScript | Python |
|---|---|---|
| allocate (optionally vesting until a height) | createAssignment | create_assignment |
| pay vested allocations through the treasury | distribute → sign its payouts[].wallet_transfer | distribute |
| reward run from a buried snapshot | declareDividend, payDividend | declare_dividend, pay_dividend |
| recompute a dividend table | dividendTableHash | dividend_table_hash |
| freeze, unfreeze, clawback, rule change | proposeControl → approveControl → executeControl | same, snake_case |
| reserve attestation | publishAttestation | publish_attestation |
| public log, verified | getPublicAssetLog + verifyIssuerLog | get_public_asset_log + verify_issuer_log |
| cap table at a record date | getAssetHolders({ asset, height }) | get_asset_holders(asset, height=…) |
5. Sign in with Arrekta
Accounts without passwords: the user signs a short message with their key. The message is bound to your domain, so a signature for one site is useless on another.
import { signIn, verifySignIn } from 'arrekta-sdk'
// browser
const { message, signature } = signIn(accountKey, { domain: location.host, network: 'regtest', nonce, issuedAt, expiresAt })
// server: then check that the nonce is yours and unused
const req = verifySignIn({ message, signature, domain: 'studio.example', network: 'regtest' })from arrekta_sdk import sign_in, verify_sign_in
message, signature = sign_in(key, domain="studio.example", network="regtest", nonce=nonce, issued_at=t0, expires_at=t1)
req = verify_sign_in(message, signature, domain="studio.example", network="regtest")6. Faucet (testnet and devnet)
import { createClient, claimFaucet } from 'arrekta-sdk'
await claimFaucet(createClient({ url: 'http://127.0.0.1:28090' }), { address })from arrekta_sdk import Client
Client("http://127.0.0.1:28090").claim_faucet(address)The faucet's proof of work is solved locally (solveFaucetChallenge / solve_faucet_challenge).