nora

Wallets

Register and manage whitelisted minter wallets on Solana, Ethereum, Base and Polygon. You sign every grant. Nora does not sign routine grants.

A wallet needs an on-chain whitelist grant before it can receive mints or burn BRS in your flows. This surface registers minter wallets for your organization and activates their grant. The server fixes the role to minter; the request schema has no role field. You register whitelister wallets on the dashboard, not through this API.

chainId accepts "solana" (the default), "ethereum", "base" or "polygon"; every chain is live in both environments (see BRS token). The solana and EVM (ethereum, base, polygon) arms share every path and every response shape, and differ in three fields: who signs, what you send to submit, and whether a limitMinor applies. Each section below marks the difference.

Signing model

You sign every grant. Nora does not sign routine grants and does not hold the signing key.

solana. Your whitelister wallet signs the grant transaction. It also pays the rent and the fee. Two paths complete a registration:

  • Prepared: Nora returns an unsigned transaction. You sign it with your whitelister and return it via submit.
  • Self-built: you build and broadcast your own whitelist transaction. Then you call sync with the txSignature as authorship proof.

ethereum. The signing contract is your organization's own BRSWhitelister, provisioned by Nora. There is no whitelisterWallet request field — Nora resolves the contract server-side. Nora returns wallet.calldata and transaction is null; the contract's owner wallet (reported as calldata.signerWallet) signs and broadcasts that call, and you return the resulting txHash via submit.

See the wallet whitelisting flow for the full sequence.

Daily limits (ethereum)

limitMinor is a daily limit in BRL cents — the same unit as amountCents on intents. 100000 is R$1.000,00. It is not token units; Nora scales it to the token's own decimals at the chain boundary, and every response reports cents back.

The whitelister contract clamps the requested value to your organization's on-chain ceiling. A clamp is a success, not an error. The wallet becomes active with the effective (lower) limitMinor and a statusReason such as:

limit_clamped: requested 200000000, effective 100000000 (the whitelister's on-chain ceiling)

Once a wallet is active, its limitMinor cannot be changed through any /v2 endpoint. prepare returns 200 with calldata: null — a silent no-op, not an error. Raising or lowering an active wallet's limit requires Nora support.

Wallet states

StateMeaning
pendingRegistered. The grant is not yet proven on-chain.
activeThe grant was confirmed and proven. It passes intent preflights only when reconciliationStatus is synced.
revoked / bannedThe grant is gone or the address is barred. On ethereum a revoked wallet can be repaired through this API — see below.

statusReason: "awaiting_sync_proof" marks a pending wallet with observed on-chain state and no authorship proof. Call sync with the grant's txSignature to activate it. (solana only — on ethereum the contract state is the proof, so sync needs nothing in the body.)

Revocation is not instant on ethereum. If the authorization is zeroed out-of-band, the first sync reports outcome: "not_on_chain" while state stays active. The row flips to revoked only once the zero is confirmed at the chain's confirmation depth (a few blocks; the exact depth per environment is an operational setting).

A revoked ethereum wallet can be repaired. POST /v2/wallets/:id/prepare issues fresh positive calldata for the last limit the wallet requested, and submit returns it to active. No re-registration is needed.

Response schemas

Wallet

FieldTypeRequiredDescription
idstring (uuid)YesNora wallet-registration ID.
addressstringYesBase58 on solana, 0x-prefixed on ethereum.
chainId"solana" | "ethereum" | "base" | "polygon"YesChain for this registration.
role"minter" | "whitelister" | "buyer" | "buyerAndMinter" | "admin"YesThe API creates only minter rows. Other roles are read-only.
labelstring | nullYesPartner-defined label.
state"pending" | "active" | "revoked" | "banned"YesCurrent registration state.
statusReasonstring | nullYesDiagnostic for a pending registration or an on-chain drift. It can include "awaiting_sync_proof", "awaiting_onchain_confirmation", "address_unavailable", "not_on_chain", or (ethereum) "limit_clamped: …".
reconciliationStatus"synced" | "missing_onchain" | "drifted"YesAgreement between the row and on-chain state.
whitelisterWalletstring | nullYesThe wallet that signs the grant. Your whitelister on solana; the whitelister contract's owner on ethereum.
limitMinorinteger | nullYesEffective, already-clamped daily limit in BRL cents. Always null on solana.
calldataCalldata | nullYesThe EVM call still awaiting your broadcast. Always null on solana, and null on ethereum when there is nothing left to sign.
createdAtstringYesISO 8601 creation time.
updatedAtstringYesISO 8601 last-update time.

Operation

FieldTypeRequiredDescription
idstring (uuid)YesOperation ID.
operationType"grant" | "regrant" | "accept_onchain" | "revoke" | "ban" | "repair"YesAction represented by this operation.
status"prepared" | "linked_existing" | "submitted" | "confirmed" | "failed" | "expired"YesCurrent operation state.
txSignaturestring | nullYesBroadcast transaction signature, when available.
errorMessagestring | nullYesFailure diagnostic, when available.
createdAtstringYesISO 8601 creation time.
updatedAtstringYesISO 8601 last-update time.

Transaction (solana)

transaction is the solana preparation. It is always null on ethereum — the EVM preparation lives at wallet.calldata instead. transaction is also null when the wallet has no current preparation.

FieldTypeRequiredDescription
unsignedTransactionstringYesBase64-encoded Solana transaction.
blockhashstringYesRecent blockhash used by the preparation.
lastValidBlockHeightintegerYesLast block height at which the transaction is valid.
expiresAtstringYesISO 8601 expiration time.
signerWalletstringYesBound whitelister and transaction fee payer.
instructionInstruction | nullYesDecomposed grant instruction for transaction composition. null when the stored preparation is not decodable.

Instruction has these fields:

FieldTypeRequiredDescription
programIdstringYesSolana program address.
keysInstructionKey[]YesOrdered instruction accounts.
datastringYesBase64-encoded instruction data.

Each InstructionKey has pubkey (string), isSigner (boolean), and isWritable (boolean). Preserve the array order when you build a transaction.

Calldata (ethereum)

wallet.calldata is the EVM sibling of transaction: the exact call you send from signerWallet. It is null on solana, and null on ethereum whenever there is nothing left to sign (including on an active wallet).

FieldTypeRequiredDescription
tostringYesYour organization's BRSWhitelister contract.
datastringYesABI-encoded call data, 0x-prefixed.
chainRefstringYesCAIP-2 ref of the chain the call is valid on — "ethereum" alone cannot say Sepolia vs mainnet.
signerWalletstringYesThe only wallet that may send it: the whitelister contract's owner. Nora does not hold this key.
limitMinorintegerYesThe requested daily limit in BRL cents, before the on-chain clamp.
expiresAtstringYesISO 8601. The window is 24 hours. Past it, submit returns 410 and you call prepare again.

Idempotency flags

Create and submit responses include two booleans:

  • linkedExisting is true when Nora finds a proven on-chain grant and activates the wallet without a new broadcast.
  • replayed is true when Nora returns the stored response for the same Idempotency-Key and request body. It is false for the first execution.

GET /v2/wallets

List your registered wallets. Whitelister rows appear read-only.

The list returns only minter and whitelister rows. A detail request can also return a legacy read-only role.

QueryTypeNotes
stateenumpending | active | revoked | banned
roleenumminter | whitelister

Response 200

{ wallets: Wallet[] }

GET /v2/wallets/:id

Wallet detail.

Response 200

{ wallet: Wallet, transaction: Transaction | null, operations: Operation[] }

transaction holds the current preparation (unsignedTransaction, blockhash, lastValidBlockHeight, expiresAt, signerWallet, instruction) or null. instruction is the decomposed grant instruction (programId, keys, data in base64) for partners who compose their own transaction. operations contains at most 25 records, newest first. Poll this endpoint to observe state transitions; do not use it as an audit-history feed.

POST /v2/wallets

Register a minter wallet. The response stages the grant you must sign.

Headers

HeaderRequiredNotes
X-API-KeyYes
Idempotency-KeyYesUUID. The same key with a different payload returns 409.
Content-TypeYesapplication/json

Request body — solana

FieldTypeRequiredDescription
addressstringYesBase58 wallet to whitelist as minter.
whitelisterWalletstringYesYour active whitelister. It signs the grant.
chainId"solana"NoDefault "solana".
labelstringNoUp to 64 characters.

Request body — ethereum

FieldTypeRequiredDescription
chainId"ethereum" | "base" | "polygon"YesRequired here — it is the discriminator that picks this arm, so it has no default.
addressstringYes0x-prefixed wallet to whitelist as minter.
limitMinorintegerYesRequested daily limit in BRL cents. 100000 is R$1.000,00. Positive integer.
labelstringNoUp to 64 characters.

This body is strict. There is no whitelisterWallet on the ethereum arm — Nora resolves your organization's whitelister contract server-side, and sending the field returns 400. If no contract is provisioned for your organization yet, the call returns 409 with details.code of WHITELISTER_NOT_PROVISIONED.

Response 200 / 201

{ wallet: Wallet, operation: Operation | null, transaction: Transaction | null, linkedExisting: boolean, replayed: boolean }

solana. When transaction is present, transaction.unsignedTransaction is base64. Its feePayer is your whitelister. transaction.instruction carries the same grant decomposed (programId, keys, data) for your own composition.

ethereum / base / polygon. transaction is always null. When a grant is needed it is at wallet.calldata — send it from calldata.signerWallet on the network calldata.chainRef names, then call submit with the txHash you get back.

solana. If a proven grant already exists on-chain at create time, the wallet activates without a broadcast, there is nothing to sign, and linkedExisting is true.

The EVM create path reads the chain. If your whitelister does not yet authorize the address, the wallet comes back pending with wallet.calldata. If it already authorizes it, the wallet comes back active at the on-chain limit with calldata: null — there is nothing to sign and nothing to submit. Branch on wallet.calldata: null means the wallet is already whitelisted and you are done; an object means send it and then call submit.

POST /v2/wallets/:id/prepare

Return the current preparation, or a fresh one after the old one expired. The bound signer never changes.

solana. Returns the same preparation, including its decomposed instruction, while blockHeight is at or below lastValidBlockHeight. It generates a new one only after expiry.

ethereum. Returns the same wallet.calldata while its 24-hour window is open, and re-prepares it — same requested limit, same signer — once the window closed or a submit failed. Two cases return calldata: null:

  • The wallet is active. There is nothing left to sign, and this route cannot change an active wallet's limit. 200, not an error.
  • A grant is already live on-chain. Nora adopts it and activated is true.

A revoked wallet re-prepares fresh positive calldata for the last limit it requested — submit it and the wallet returns to active.

If the wallet has no recorded limit to re-prepare from, the call returns 409 with details.code of LIMIT_REQUIRED: re-register the address with a limitMinor.

Response 200

{ wallet: Wallet, transaction: Transaction | null, activated: boolean }

On solana, transaction is null when Nora observes an on-chain grant; if activated is false, call sync with the grant transaction signature to provide authorship proof.

POST /v2/wallets/:id/submit

Complete the grant. Requires Idempotency-Key, with the same rules as create. Send the field your wallet's chain uses — the other one returns 400 naming the expected field.

Request body — solana

FieldTypeRequiredDescription
signedTransactionstringYesThe prepared transaction, signed, base64.

Nora verifies the signed bytes against the preparation and broadcasts them. The fee payer, the blockhash, and the instructions must match. You may add only bounded ComputeBudget instructions. A mismatch returns details.code of TRANSACTION_MISMATCH. An expired preparation returns details.code of TRANSACTION_EXPIRED while the preparation is still stored; once it has been dropped the same case is a plain 400 BAD_REQUEST with message: "Wallet has no pending prepared transaction to submit — call prepare (or sync, to adopt an existing grant with proof) first". Either way: call prepare, re-sign, and submit again with a new Idempotency-Key. A Solana preparation lives only as long as its blockhash, well under the time a human signer or a multisig round-trip takes; call prepare right before signing.

Request body — ethereum

FieldTypeRequiredDescription
txHashstringYesThe hash of the transaction you broadcast — 0x plus 64 hex characters. Not signedTransaction, not txSignature.

You broadcast the calldata yourself; Nora verifies the receipt. The emitted grant must match the prepared recipient and the clamped limit.

OutcomeResponse
Confirmed200. The wallet is active (or revoked for a zero grant) with the effective limitMinor.
Not mined or not yet confirmed409 RECEIPT_PENDING with a Retry-After header. Retry with the same txHash and Idempotency-Key.
Preparation past its window410 PREPARATION_EXPIRED. Call prepare for fresh calldata.
Receipt does not satisfy the grant422 VERIFICATION_FAILED. Reverted, wrong contract, wrong recipient, or a limit that does not match the clamped value. prepare again.
Receipt fine, but another transaction changed the authorization since422 RECIPIENT_STATE_MISMATCH. Read the wallet; prepare again if you still want the grant.
A different txHash for the same operation409 TX_HASH_MISMATCH. Resend the original hash.
Preparation not bound to a chain height409 OPERATION_UNBOUND. Call prepare again and submit against the new calldata.

Response 200

{ wallet: Wallet, operation: Operation, linkedExisting: boolean, replayed: boolean }

POST /v2/wallets/:id/sync

Reconcile the wallet from on-chain state. Use it after you broadcast your own transaction, or anytime as a drift re-check.

FieldTypeRequiredDescription
txSignaturestringNosolana only. The grant transaction's signature. Required to activate a grant Nora did not broadcast.

solana. Activation requires authorship proof. The grant must bind to this registration's own preparation, or the txSignature's WhitelistUpdatedEvent.updated_by must be one of your active whitelisters. When Nora observes a grant on-chain that it cannot attribute, the call returns 422 with details.code of SYNC_PROOF_REQUIRED and the wallet stays pending with statusReason: "awaiting_sync_proof". When there is no grant on-chain at all, the call is a 200 with outcome: "not_on_chain" and the row is unchanged.

ethereum. No proof is needed and the body can be empty — Nora reads the recipient's authorization straight from your whitelister contract, and the contract state is the proof. A live limit activates the wallet with that effective value. A zeroed authorization does not flip the row on the first read: the outcome is not_on_chain and the state stays active until the zero is confirmed at the chain's confirmation depth.

Response 200

{ outcome: "activated" | "already_synced" | "not_on_chain", wallet: Wallet, operation: Operation | null }

operation can be null or a record for any outcome (a not_on_chain read on Ethereum can carry a confirmed repair record, for example). Treat outcome as the result and operation as an audit detail, never as a state change.

DELETE /v2/wallets/:id

Cancel a pending registration, on either chain. The row and its operations are hard-deleted and the address is freed: a follow-up GET returns 404, and you can register the same address again as a fresh registration.

This API cannot delete active wallets — that is a 409 with details.code of WALLET_NOT_PENDING, or ALREADY_ACTIVE when the pre-delete on-chain check already sees the grant.

Response 200

{ id: string }

Error envelope

Every wallet endpoint returns this shape for non-2xx responses:

{
  "code": "CONFLICT",
  "message": "The prepared transaction has expired.",
  "details": { "code": "TRANSACTION_EXPIRED" }
}

code and message are always present, except on structural validation failures (missing Idempotency-Key, mistyped body), which come back as { "data": …, "error": [{ "code", "path", "message" }], "success": false }. details is optional. For a wallet-specific error, use details.code for recovery. Use the top-level code only for the general error category.

Machine error codes

CodeMeaning
WHITELISTER_NOT_FOUNDwhitelisterWallet is not registered for your org and environment.
WHITELISTER_NOT_ACTIVEThe whitelister exists and is not active.
WHITELISTER_NOT_PROVISIONED(ethereum) No whitelister contract is provisioned for your org yet. Contact Nora.
WALLET_ALREADY_REGISTEREDThe address already has a row in your org.
ADDRESS_UNAVAILABLEThe address cannot be registered.
WALLET_BANNEDThe address is banned on-chain.
TRANSACTION_EXPIREDThe preparation's blockhash expired. Call prepare again.
TRANSACTION_MISMATCHThe signed bytes do not match the preparation.
ALREADY_ACTIVEThe wallet is already active.
NOT_ON_CHAINsync found no grant, or submit could not confirm an exact minter grant. Poll the wallet or call sync.
SYNC_PROOF_REQUIREDOn-chain state exists and authorship proof is missing.
IDEMPOTENCY_KEY_MISMATCHThe same Idempotency-Key arrived with a different payload.
WALLET_NOT_PENDINGOnly pending registrations can be cancelled.
WALLET_NOT_MANAGEDThe wallet is not a minter registration managed by this API.
WALLET_CAP_EXCEEDEDThe organization reached its active and pending minter-wallet limit.
RECEIPT_PENDING(ethereum) The receipt is not confirmed yet. Retry submit with the same txHash and Idempotency-Key after Retry-After.
PREPARATION_EXPIRED(ethereum) The prepared calldata is past its 24-hour window. Call prepare.
VERIFICATION_FAILED(ethereum) The receipt does not satisfy the prepared grant.
LIMIT_REQUIRED(ethereum) The wallet has no recorded limit to re-prepare from. Re-register the address with a limitMinor.
RECIPIENT_STATE_MISMATCH(ethereum, submit) The receipt verified, but another transaction changed the recipient's authorization since. Read the wallet and prepare again if you still want the grant.
TX_HASH_MISMATCH(ethereum, submit) A different txHash was sent for an operation that already has one. Resend the original.
OPERATION_UNBOUND(ethereum) The preparation could not be bound to a chain height. Call prepare again and submit against the new calldata.
OPERATION_IN_FLIGHT(prepare) A prepared or submitted grant is still awaiting its receipt. Submit it, or let it expire, before preparing another.

Rate limits

prepare, submit, and sync share a dedicated limit of 20 requests per minute per API key. This limit is in addition to the global per-key limit. Poll GET /v2/wallets/:id for status. Do not poll sync.

On this page