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).
| Kind | What it is | Control | Verification |
|---|---|---|---|
server_key | A customer-controlled EOA (a single key, 1-of-1). | The private key. | Registered unverified, then proven by signing a server challenge (proof-of-possession). |
safe | A Gnosis Safe whose N-of-M owner set is its control. | The owner quorum. | Verified at registration; there is no proof step. |
| Purpose | Meaning | May hold |
|---|---|---|
operator (default) | A first-class controller of the token. | Any token role, including admin. |
service_account | An 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 form | Resolves 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.
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).
- Request a challenge. Call
POST /v1/tokenization/authorities/{id}/proof-challenge. It returns an opaquechallengetoken, the exactmessagethe authority's EOA must sign, and anexpires_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. - Sign and submit. The EOA signs the message with a standard EIP-191 personal-sign
(
account.signMessage({ message })), producing a 65-byte signature. Submit thechallengeand thesignaturetoPOST /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 stampsverified_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}/policiesscopes 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-mintmints gaslessly through that scoped role. A request above the confirmed on-chain cap is rejected withtokenization_policy_exceededbefore 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}/contractslists the token's three control contracts (thetokenitself, itsaccess_registry, and itsroles_modifier) with each resolved on-chain address and anabi_namekey (ERC20XGasless,AccessRegistry, orRoles). The client reads the full function list from the ABI barrel the SDK ships, so it never hardcodes an address. Theroles_modifierentry is omitted until its proxy address is resolvable.GET /v1/tokenization/tokens/{id}/readcalls one view or pure function on one of those three contracts (acontract,function, and JSONargsquery). 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 withvalidation_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.
| Code | HTTP | When |
|---|---|---|
tokenization_not_authorized | 403 | The caller is in the right tenant
but lacks the tokenize_admin role. |
tokenization_authority_not_found | 404 | An unknown authority id in the caller's workspace (a genuine miss and a cross-workspace id are indistinguishable by design). |
tokenization_authority_kind_mismatch | 422 | A proof endpoint was
called on a safe, which has no proof-of-possession step. |
tokenization_proof_invalid | 422 | A forged or expired challenge, or a signature that does not recover to the registered address. |
tokenization_authority_already_verified | 409 | A proof was submitted for an authority that is already verified (a server key is provable exactly once). |
role_not_allowed_for_service_account | 422 | A raw role grant targeted a
service_account authority. Use a mint-cap policy instead. |
tokenization_raw_call_invalid | 422 | A raw Safe transaction named an unknown contract or function, or arguments whose shape does not match. |
Related
- Access and Policy: named roles, mint-cap policies, and the access registry an authority acts on.
- Deploy and Mint: the deploy saga and the signing flow every operation follows.
- Treasury and Safes: the N-of-M Safes an authority can point at.
- Non-custodial Model: why every write is gated by a signature, not the platform.