XKOVA Docs

Token Authorities

A token's roles (minter, burner, pauser, admin) are held by addresses, not by users in a database. A token authority is a registered address the workspace trusts to hold those roles. This page covers the two axes that describe an authority (its kind and its purpose), how you prove control of a server key before the platform trusts it, the capped minter-only service account, and the read and write surfaces for a token's control contracts.

The Two Axes: Kind and Purpose

Every authority is described by two independent choices. Its kind is what the address is, and it fixes how control is proven. Its purpose is what the address is allowed to hold.

An authority is workspace scoped: you register it once, and it is usable across every token in the workspace. Managing authorities is tenant scoped, requires a step-up (AAL2) authentication for mutations, and requires the tokenize_admin role (tenant admins inherit it).

KindWhat it isControlVerification
server_keyA customer-controlled EOA (a single key, 1-of-1). The private key.Registered unverified, then proven by signing a server challenge (proof-of-possession).
safeA Gnosis Safe whose N-of-M owner set is its control. The owner quorum.Verified at registration; there is no proof step.
PurposeMeaningMay hold
operator (default)A first-class controller of the token. Any token role, including admin.
service_accountAn integrator-held automation EOA for unattended minting. Only a policy-capped minter, never admin.

The two axes are not fully independent: a service_account must be a server_key (a Safe cannot be a service account). Registering a service_account with any other kind is rejected with validation_failed, and a database constraint backs the rule.

Registering an Authority

You register an authority with a single call to POST /v1/tokenization/authorities. What the body carries depends on the kind. A server_key supplies the EOA address (its EIP-55 checksum is re-validated server side) and is registered unverified. A safe supplies a safeRef and is verified immediately.

The safeRef field is polymorphic. It resolves the Safe from any of three forms, so you can point at a Safe XKOVA manages for you or at an external one you assert.

safeRef formResolves to
msf_...A managed Safe by its id.
vlt_...The same managed Safe under its treasury-vault id (reusing a treasury Safe as a token controller).
0x... (40 hex)An external Safe you assert for this workspace. It is stored as a bare address with no managed link.

Registering an authority grants nothing on its own. The admin Safe's N-of-M signature still gates every role grant on chain, so trusting an operator's assertion of an external Safe is safe: the assertion only records who may later be granted a role, not the grant itself.

If you want a fresh controller rather than a reference to an existing Safe, use POST /v1/tokenization/authorities/control-safes. It deploys a new Safe dedicated to token control (with the staff signers and N-of-M threshold you supply, the same quorum flow as a treasury vault) and registers it as a verified safe authority in one step. The Safe deploys asynchronously; the returned address is the deterministic (counterfactual) address, and a role grant against it waits for the Safe to be live on chain.

Registration is keyed on the pair of workspace and address. When a role is granted to an address, XKOVA auto-creates a lightweight authority row for it on chain confirmation. Registering that same address later enriches the existing row (its label, kind, and Safe linkage) rather than creating a duplicate. The address is the join key in both directions.

The authority record returns a tka_ prefixed id, its kind and purpose, the checksummed address, a verified_at timestamp (null for an unproven server key), and, for a Safe, its safe_purpose and a managed_safe_id (msf_). Fetch one authority with GET /v1/tokenization/authorities/{id} to also read its active role matrix: each role it holds, the token it holds it on, and when it was granted.

Proving Control of a Server Key

A server_key is registered unverified. Before the platform trusts it to hold a role, the customer must prove they control the key. Proof-of-possession is a two-call flow, and it does not apply to a safe (a Safe's control is its on-chain owner set, verified at registration; calling the proof endpoints on a Safe returns tokenization_authority_kind_mismatch).

  1. Request a challenge. Call POST /v1/tokenization/authorities/{id}/proof-challenge. It returns an opaque challenge token, the exact message the authority's EOA must sign, and an expires_at. The challenge is a stateless signed token; no row is stored to remember it. The message binds a fixed domain string, the authority id, the workspace id, and a random nonce, so a signature collected for it cannot be replayed for another authority or purpose.
  2. Sign and submit. The EOA signs the message with a standard EIP-191 personal-sign (account.signMessage({ message })), producing a 65-byte signature. Submit the challenge and the signature to POST /v1/tokenization/authorities/{id}/proof. The api verifies the challenge and its expiry, recovers the signer from the signature over the exact message, and accepts only if the recovered address equals the authority's registered address. On success it stamps verified_at.

A server key is provable exactly once. Its first successful proof is permanent, so a proof submitted for an already-verified authority returns tokenization_authority_already_verified. A forged or expired challenge, or a signature that does not recover to the registered address, returns tokenization_proof_invalid (the same code for both, so a caller cannot distinguish a bad challenge from a bad signature). The proof endpoints are an authenticated integrator or operator route, not a capability link: the recovered signature is the cryptographic gate, on top of the authenticated, step-up session.

Capped Minter-only Service Accounts

A service account lets an integrator's backend mint unattended, without holding an admin key. It is a server_key authority registered with purpose: service_account. After you prove control of it, you do not grant it a role through the raw grant path. Instead you scope it with a mint-cap policy.

  • Set the policy. POST /v1/tokenization/tokens/{id}/policies scopes the authority on the token's per-token roles modifier so it may mint gaslessly within an on-chain per-call amount cap. This is enforced on chain, not just in the mirror.
  • Mint within the cap. POST /v1/tokenization/tokens/{id}/policy-mint mints gaslessly through that scoped role. A request above the confirmed on-chain cap is rejected with tokenization_policy_exceeded before it is relayed.

The raw grant path is deliberately closed to a service account. Granting an ordinary role would produce an uncapped on-chain minter, so a grant that targets a service_account is rejected with role_not_allowed_for_service_account regardless of which role was named. A service account can hold a capped minter and nothing else, by construction. See Access and Policy for how mint-cap policies work.

Reading a Token's Control Surface

Every token exposes a generic, read-only contract explorer over its own control contracts. It is scoped strictly to the token's own contracts: an arbitrary address is never reachable. The reads require the tokenize_minter role and carry no step-up, because a view call is not a signed mutation.

  • GET /v1/tokenization/tokens/{id}/contracts lists the token's three control contracts (the token itself, its access_registry, and its roles_modifier) with each resolved on-chain address and an abi_name key (ERC20XGasless, AccessRegistry, or Roles). The client reads the full function list from the ABI barrel the SDK ships, so it never hardcodes an address. The roles_modifier entry is omitted until its proxy address is resolvable.
  • GET /v1/tokenization/tokens/{id}/read calls one view or pure function on one of those three contracts (a contract, function, and JSON args query). The result is ABI-decoded with every integer stringified, so it is JSON-safe. An unknown contract, an unknown function, or a state-mutating (non-view) function is rejected with validation_failed.

The Raw Safe Transaction Escape Hatch

The typed lifecycle (mint, burn, roles, access list, pause, policy) covers the common admin actions. The frozen token, access-registry, and roles contracts also expose admin functions those verticals do not wrap, such as updating the contract URI, changing the trusted forwarder, or a roles scope tweak. Rather than ship a bespoke endpoint per function, POST /v1/tokenization/tokens/{id}/raw-safe-tx lets an admin compose any of them.

The escape hatch is arbitrary function on a fixed contract, not an arbitrary call. The contract is one of the token's three own control contracts by enum, and the target address is server-resolved from that enum, so an arbitrary address can never be reached. The inner calldata is encoded from the committed ABI, so a function not in that contract's ABI, or arguments whose count or shape do not match its inputs, is rejected with tokenization_raw_call_invalid before any transaction is built.

Because it is composed by an admin, it is the most privileged tokenization act: it requires the tokenize_admin role and a step-up authentication. It is otherwise a normal signing operation. It lands in pending_sign and cannot broadcast without the admin Safe's N-of-M owner signatures (submitted to the shared operation signatures endpoint) and the XKOVA Relayer, so on-chain authorization is the unchanged, fully non-custodial Safe gate. It moves no value generically, so it sits outside the value-movement compliance gate, and it writes no mirror rows: the operator chose a raw call, so on chain is the source of truth. Every raw call is recorded in the audit log.

See Deploy and Mint for the shared signing and settlement flow every tokenization operation follows, and Non-custodial Model for why the Safe signature, not the platform, is the authority behind every write.

Errors Worth Handling

The error catalog documents every code; these are the ones this surface most commonly returns.

CodeHTTPWhen
tokenization_not_authorized403The caller is in the right tenant but lacks the tokenize_admin role.
tokenization_authority_not_found404An unknown authority id in the caller's workspace (a genuine miss and a cross-workspace id are indistinguishable by design).
tokenization_authority_kind_mismatch422A proof endpoint was called on a safe, which has no proof-of-possession step.
tokenization_proof_invalid422A forged or expired challenge, or a signature that does not recover to the registered address.
tokenization_authority_already_verified409A proof was submitted for an authority that is already verified (a server key is provable exactly once).
role_not_allowed_for_service_account422A raw role grant targeted a service_account authority. Use a mint-cap policy instead.
tokenization_raw_call_invalid422A raw Safe transaction named an unknown contract or function, or arguments whose shape does not match.

Related

For the exact request and response shapes, see the operations list or try the calls live in the interactive reference.