Scoped Tokens & Permissions
Arca uses an IAM-style, resource/capabilities permission model inspired by AWS. Both scoped JWT tokens (for end-users) and scoped API keys (for services) carry policy statements that grant specific actions on specific Arca path patterns.
Policy Model
A scope contains one or more policy statements. Each statement has an effect ("Allow" or "Deny"), a set of actions, and a set of resources (Arca object path patterns):
{ "statements": [ { "effect": "Allow", "actions": ["arca:TransferFrom", "arca:ReceiveTo"], "resources": ["/users/u123/*"] }, { "effect": "Allow", "actions": ["arca:Read"], "resources": ["*"] }, { "effect": "Deny", "actions": ["arca:*"], "resources": ["/_internal/*"] } ]}Anything not explicitly allowed is denied by default. Deny statements act as safety rails — they override any Allow statements. Use them to protect sensitive paths even when a broad Allow is in effect.
Action Catalog
Retrieve the full catalog programmatically via GET /api/v1/permissions (no auth required).
Object Lifecycle
| Action | Description | Checked against |
|---|---|---|
arca:CreateObject | Create a new Arca object | Path being created |
arca:UpdateObject | Update an Arca object (e.g., labels) | Object being updated |
arca:DeleteObject | Delete an Arca object | Object being deleted |
Balance (directional)
| Action | Description | Checked against |
|---|---|---|
arca:TransferFrom | Debit funds (source side) | Source path |
arca:ReceiveTo | Receive funds (deposit, transfer credit, or sweep) | Target path |
arca:WithdrawFrom | Initiate outbound withdrawal | Source path |
Read / Observe
| Action | Description |
|---|---|
arca:ReadObject | View object metadata and status |
arca:ReadBalance | View object balances |
arca:ReadOperation | View operations |
arca:ReadEvent | View events |
arca:ReadDelta | View state deltas |
arca:Subscribe | WebSocket real-time stream |
arca:ReadAuditLog | View audit log entries |
Exchange
| Action | Description | Checked against |
|---|---|---|
arca:PlaceOrder | Place an exchange order | Exchange object |
arca:CancelOrder | Cancel an exchange order | Exchange object |
arca:ReadExchange | View exchange state, positions, and orders | Exchange object |
Convenience Aliases
Aliases expand to individual actions. Named aliases are expanded at mint time (stored in the JWT). The wildcard arca:* is stored as-is and matches any action at check time.
| Alias | Expands to |
|---|---|
arca:Read | All arca:Read* + arca:Subscribe + arca:ReadAuditLog |
arca:Transfer | arca:TransferFrom + arca:ReceiveTo |
arca:Fund | arca:ReceiveTo + arca:WithdrawFrom |
arca:Lifecycle | arca:CreateObject + arca:UpdateObject + arca:DeleteObject |
arca:Exchange | arca:PlaceOrder + arca:CancelOrder + arca:ReadExchange |
arca:Write | All write actions |
arca:* | Everything (evaluated at check time) |
Resource Patterns
/treasury/usd— exact match/users/*— matches the prefix and all paths below it*— matches all resources
Path safety note: Arca paths have no directory-traversal semantics. The segments . and .. are treated as literal characters. There is no path normalization — what you see is what matches. This is a deliberate security invariant.
Authorization Principles
- Deny by default. Anything not explicitly granted by an Allow statement is denied.
- Explicit Deny overrides Allow. If any Deny statement matches an (action, resource) pair, the request is blocked — regardless of any Allow statements. Use Deny as a safety rail to protect sensitive paths (e.g.,
/_internal/*). - Actions are directional and per-resource. For operations touching multiple objects, each side is checked independently. A transfer checks
arca:TransferFromon the source andarca:ReceiveToon the target. - Inbound permissions are unified.
arca:ReceiveTocovers deposits, transfer credits, and sweeps. The balance outcome is identical regardless of channel. Outbound actions (arca:TransferFrom,arca:WithdrawFrom) remain separate because they cross different trust boundaries. - Delete + sweep is compound authorization. Deleting an object with balance requires
arca:DeleteObjecton the source andarca:ReceiveToon the sweep target. Without a sweep target,arca:DeleteObjectalone suffices for zero-balance objects. - Realm is a boundary, not a resource. Scoped tokens are locked to one realm. Resource patterns do not encode realm — this simplifies matching.
Mint Scoped Token
POST/api/v1/auth/tokenJWT / API Key
Mint a scoped JWT bound to a single realm with IAM-style policy statements. The resulting token is passed from the builder's backend to the end-user's frontend.
Request Body
realmIdstringrequiredsubstringrequiredscopeobjectrequiredstatements array. Each statement has effect ("Allow" or "Deny", default "Allow"), actions (action strings or aliases), and resources (path patterns).expirationMinutesnumber60.Response 201 Created
{ "success": true, "data": { "token": "eyJhbGciOiJIUzI1NiIs...", "expiresAt": "2026-02-14T15:00:00Z" }}Example
# Builder's backend mints a token for user "alice"curl -X POST http://localhost:3052/api/v1/auth/token \ -H "Authorization: Bearer arca_78ae7276_..." \ -H "Content-Type: application/json" \ -d '{ "realmId": "6d25623e-...", "sub": "alice", "scope": { "statements": [ { "effect": "Allow", "actions": ["arca:Read"], "resources": ["*"] }, { "effect": "Allow", "actions": ["arca:TransferFrom", "arca:ReceiveTo"], "resources": ["/users/alice/*"] }, { "effect": "Deny", "actions": ["arca:*"], "resources": ["/_internal/*"] } ] }, "expirationMinutes": 30 }'Permissions Catalog
GET/api/v1/permissionsNone
Returns the full action catalog with descriptions, grouped by category, plus all available aliases. No authentication required.
Auth Audit Log
GET/api/v1/auth/auditJWT / API Key
Returns a paginated list of authentication and authorization events for the authenticated builder. Tracks sign-ins, token minting, API key usage, and permission denials.
Query Parameters
eventTypestringsign_in, token_minted, api_key_authenticated, permission_denied.realmIdstringsincestringuntilstringsuccessbooleantrue for successful events, false for denials.limitnumberoffsetnumberResponse 200 OK
{ "success": true, "data": { "entries": [ { "id": "...", "builderId": "...", "eventType": "token_minted", "actorType": "builder", "actorId": "...", "realmId": "...", "subject": "alice", "scopeSummary": "{...}", "tokenJti": "...", "expiresAt": "2026-02-21T15:00:00Z", "success": true, "operationCount": 12, "createdAt": "2026-02-21T14:00:00Z" } ], "total": 42 }}Scoped tokens can access this endpoint with the arca:ReadAuditLog action (included in the arca:Read alias). The operationCount field on token_minted entries shows how many operations were triggered by that token's subject.
Credential Scope Lookup
GET/api/v1/auth/audit/scopeJWT / API Key
Returns the permissions (scope) of a specific credential — either a scoped JWT (by tokenJti) or an API key (by apiKeyId). Use this to inspect what permissions a credential carried when it was used to perform operations.
Query Parameters
tokenJtistringtoken_minted audit entry to retrieve the scope.apiKeyIdstringapi_keys table.Response 200 OK
{ "success": true, "data": { "credentialType": "scoped_token", "credentialId": "abc-123-jti", "subject": "alice", "scope": "{\"statements\":[...]}", "fullAccess": false, "createdAt": "2026-02-21T14:00:00Z", "expiresAt": "2026-02-21T15:00:00Z" }}Requires the arca:ReadAuditLog permission. For API keys without a scope, fullAccess is true and scope is null.
Scope Enforcement
When a scoped token (or scoped API key) is used, the API enforces:
- Realm lock: Scoped tokens can only access the realm specified at mint time. Requests to other realms return
403 FORBIDDEN. - Policy evaluation: Each operation produces one or more (action, resource) pairs. For each pair: (a) if any Deny statement matches, access is blocked; (b) if any Allow statement matches, access is granted; (c) otherwise, access is denied (implicit deny). All pairs must pass for the request to succeed.
- Resource matching: Requested paths must match at least one pattern in the granting statement. Patterns ending in
/*match all descendants.
Integration Flow
BuilderBackend ArcaAPI EndUserFrontend | | | |--- POST /auth/token --->| | |<-- scoped JWT ----------| | | | | |--- hand JWT to user --->| | | | | | |<-- POST /transfer -| | | (Bearer scoped) | | |-- verify + enforce -| | | | | |--- response ------>|Team & Org Management
Every Arca account belongs to an organization. An org owns one or more realms (demo, production) and hasmembers with defined roles. API keys are org-scoped and grant access to all realms the key holder can reach.
Roles
Org members have one of four roles, listed from most to least privileged:
| Capability | Owner | Admin | Developer | Viewer |
|---|---|---|---|---|
| View realms, objects, balances, operations | Yes | Yes | Yes | Yes |
| Create/modify objects, transfers, orders | Yes | Yes | Yes | No |
| Delete objects | Yes | Yes | No | No |
| Invite/remove members, manage API keys | Yes | Yes | No | No |
| Transfer org ownership | Yes | No | No | No |
When a member accesses a realm, their org role is mapped to a set of arca:* permissions automatically. Owners and admins get unrestricted access. Developers can do everything except delete objects. Viewers are read-only.
Invitations
Only admins and owners can invite new members. The flow:
- Invite — an admin calls
POST /api/v1/org/invitationswith the recipient's email and desired role. The invitee's role cannot exceed the inviter's. - Email — the recipient receives an invitation link with a JWT token (valid ~7 days).
- Preview — the recipient can view the org name and role without signing in (
GET /api/v1/invitations/{token}/preview). - Accept — the recipient signs in and accepts the invitation (
POST /api/v1/invitations/{token}/accept). They become a member immediately.
Admins can list pending invitations with GET /api/v1/org/invitations and revoke them with DELETE /api/v1/org/invitations/{id}. Recipients can check their own pending invitations with GET /api/v1/invitations/pending.
Realm-Type Scoping
When inviting or updating a member, you can restrict them to specific realm types (e.g., demo only). A scoped member can only access realms matching their allowed types.
Subset rule: you can only assign realm-type scopes that are equal to or narrower than your own. An admin scoped to demo cannot invite someone with production access.
This is useful for giving contractors or junior developers access to demo environments without exposing production data.
API Key Management
API keys are org-scoped and authenticate server-to-server calls. Manage them through the portal or API:
- Create:
POST /api/v1/api-keys— returns the key value once. Store it securely. - List:
GET /api/v1/api-keys— shows all keys (values are masked). - Revoke:
DELETE /api/v1/api-keys/{id}— immediately invalidates the key.
To rotate a key: create a new one, update your application to use it, verify it works, then revoke the old key. Never revoke before your application has switched over.
API keys grant full access to every realm in the org. Use scoped tokens (see above) when you need to restrict access to specific resources or actions.
Signing Keys (Operation Non-Repudiation)
Signing keys add a layer of cryptographic non-repudiation to API operations. While API keys prove authentication (who is making the request), signing keys prove authorization (that the builder explicitly approved this specific operation with these specific parameters).
Each signing key is an Ed25519 keypair. The builder generates the keypair locally and registers only the public key with Arca. The private key never leaves the builder's environment.
Key Lifecycle
| State | Description |
|---|---|
pending | Key registered but in 72-hour grace period (first key per org activates immediately) |
active | Key can be used to sign operations |
revoked | Key permanently disabled by the builder |
rotated | Key replaced by a newer active key (automatically set during activation) |
frozen | Key suspended by Arca admin (security incident) |
Grace Period
When a second (or subsequent) key is registered, it enters a 72-hour grace period before activation. This window allows the builder (or Arca) to detect and cancel unauthorized key registrations. Practice realms skip the grace period for faster iteration.
During activation, the previously active key is automatically transitioned to rotated state.
Audit Trail
Every key lifecycle event (registration, activation, revocation, freeze) is recorded in a hash-chained event log. Each event includes a SHA-256 hash linking it to the previous event, creating a tamper-evident chain. View key events via GET /api/v1/signing-keys/:id/events.
Key Generation
Generate an Ed25519 keypair using OpenSSL or any Ed25519 library:
# Generate private keyopenssl genpkey -algorithm Ed25519 -out signing-key.pem
# Extract public key (base64)openssl pkey -in signing-key.pem -pubout -outform DER | base64Or in Node.js:
import { generateKeyPairSync } from 'crypto';const { publicKey, privateKey } = generateKeyPairSync('ed25519');const pubKeyBase64 = publicKey.export({ type: 'spki', format: 'der' }).toString('base64');// Register pubKeyBase64 via POST /api/v1/signing-keysWhen to Use Signing Keys
- Production realms: Signing keys provide cryptographic evidence that all fund movements and trades were authorized by the builder, protecting both the builder and Arca.
- Compliance: Signed operations create an auditable chain of builder intent that can be independently verified.
- Multi-person teams: When multiple team members have API access, signing keys attribute specific actions to specific individuals.
Signing is currently optional. Future releases will require signing for production realms. We recommend adopting signing keys early to avoid a migration later.
Common Patterns
Set up a 5-person startup
The founder creates the org (becomes owner). Invite the CTO and lead engineer as admins (they can manage members and API keys). Invite two developers as developers (full access except object deletion).
Give a contractor demo-only access
Invite the contractor as a developer with realm-type scope restricted to demo. They can build and test against demo realms but cannot see or touch production data.
Rotate API keys safely
- Create a new API key in the portal or via the API.
- Update your application's environment variables to use the new key.
- Deploy and verify requests succeed with the new key.
- Revoke the old key.
SDK note: org and team management methods live on the ArcaAdmin class (imported from @arca-network/sdk), not the main Arca class. Key methods: createOrg, listMembers, inviteMember, updateMember, removeMember, transferOwnership.
Ownership Transfer
Only the current owner can transfer ownership. The target must already be an admin. After transfer, the previous owner is demoted to admin.
# CLIarca org transfer-ownership --target-user-id <userId>
# APIPOST /api/v1/org/transfer-ownership{ "targetUserId": "<userId>" }Custody Model
Arca manages private keys, transaction signing, and on-chain settlement on behalf of builders. You interact with funds through the SDK and API — you never handle private keys or sign transactions directly.
Three-Layer Architecture
| Layer | Function | Security |
|---|---|---|
| Wallet Service | Generates private keys and signs transactions via a dedicated Rust gRPC service | Envelope encryption with HSM-backed key encryption keys; runs in confidential compute pods (hardware-level memory isolation) |
| Custody Pools | On-chain addresses that aggregate funds before deployment to exchanges or withdrawal | All movements recorded in the operation ledger with four-bucket conservation invariants (arriving, settled, departing, total) |
| Exchange Accounts | Funds deployed to exchanges (currently Hyperliquid) for trading | Arca controls exchange account keys; real-time position and balance tracking via streaming infrastructure |
Who Controls What
| Layer | Key Holder | Operation Initiator | Risk Bearer |
|---|---|---|---|
| Wallet Service | Arca | Platform service (on builder API request) | Arca (operational risk) |
| Custody Pools | Arca | Platform service (automated pool management) | Arca + builders (shared) |
| Exchange Accounts | Arca | Builder (via SDK/API trading operations) | Builder + their users (market risk); Arca (exchange counterparty risk) |
| Builder's Arca Objects | Arca (holds keys) | Builder (all operations via API) | Builder's risk model |
Arca holds all private keys and has technical control over all funds at every layer. Builders have operational control — you decide what transactions to execute — but not cryptographic control (you cannot sign transactions directly).
Builder vs. Arca Responsibilities
| Arca Manages | Builder Manages |
|---|---|
| Private key generation and storage | Application logic and user experience |
| Transaction signing | User authentication and identity |
| On-chain settlement and custody pools | KYC/AML compliance for your users (if applicable) |
| Ledger integrity and conservation invariants | Business logic (fee structures, limits, approvals) |
| Exchange account management | Risk management and position sizing |
| Real-time streaming and event delivery | Client-side display and error handling |
How This Compares
| Platform | Model | Builder Builds |
|---|---|---|
| Arca | Managed infrastructure — ledger, custody, trading, streaming | Application logic, UX, user auth |
| Privy | Auth + embedded wallets | Everything after authentication (ledger, trading, settlement, streaming) |
| Turnkey | Key infrastructure (signing API) | Everything above key management (ledger, custody pools, trading, streaming) |
| Fireblocks | Institutional custody + MPC | Application layer; designed for institutional treasury, not builder platforms |
For Your Legal Team
This section is informational, not legal advice.
Key facts to share with counsel when evaluating Arca:
- Arca holds all private keys and has technical custody of all funds on the platform.
- Arca is not a qualified custodian (no bank charter, trust company, or broker-dealer registration).
- Funds deployed to exchanges carry exchange counterparty risk.
- Builders are responsible for evaluating their own regulatory requirements based on jurisdiction, user base, and use case.
- Arca does not provide KYC/AML services — builders using payment links or direct integrations must handle compliance for their own users where required.
Architecture Patterns
When integrating Arca into your product, the key architectural decision is which operations your end-user's frontend performs directly against Arca (using a scoped token) versus which ones route through your backend (using an API key).
The guiding principle: Arca scopes restrict which resources and which actions, but they cannot enforce business logic. If you need to check anything beyond “does this user have permission on this resource” before allowing a write — KYC status, daily limits, manager approval, balance checks in your own system — that write must go through your backend.
Recommended: Read-Only Frontend Tokens
Mint scoped tokens with arca:Read scope for your frontend. This alias includes all read actions plus arca:Subscribe, so real-time streaming works natively. The frontend gets:
- Real-time balance, position, and order-status streaming via WebSocket
- Object, operation, event, and delta reads
- Exchange data reads (order book, positions, fills)
- Portfolio aggregation and P&L
All mutations (transfers, deposits, withdrawals, object lifecycle) go through your backend using an API key. This gives you:
- Business logic enforcement before any mutation — KYC checks, daily limits, approval flows
- Complete server-side audit trail of what was requested and why
- Minimal blast radius if a frontend token is compromised — read-only means no fund movement
// Builder's backend — mint a read-only token for the frontendconst { token } = await admin.mintToken({ realmId: realm.id, sub: user.id, scope: { statements: [ { effect: 'Allow', actions: ['arca:Read'], resources: ['*'] }, ], }, expirationMinutes: 30,});// Return 'token' to the frontend
// End-user's frontend — read and stream, no write riskconst arca = Arca.fromToken(token);const balances = await arca.watchBalances('/users/alice/');const ops = await arca.watchOperations();Advanced: Selective Write Scopes
For specific cases where the backend hop adds unacceptable latency, you can selectively add write actions to the frontend token. Consider the risk gradient:
| Risk | Actions | Considerations |
|---|---|---|
| Low | arca:PlaceOrder + arca:CancelOrder | Orders are bounded by existing balances and scoped to specific exchange paths. Acceptable for trading UIs where latency matters. |
| Medium | arca:TransferFrom | Allows fund movement between objects. Requires careful path scoping. You lose the ability to enforce per-transfer business logic (approval flows, velocity limits). |
| High | arca:WithdrawFrom, arca:CreateObject, arca:DeleteObject | Affects fund outflows and object lifecycle. Strongly recommend keeping these backend-only. A compromised token with withdrawal scope can drain funds. |
Decision Table
| Operation | Recommended | Why |
|---|---|---|
| Watch balances, positions, order status | Frontend (read-only token) | Streaming works natively, zero write risk |
| List objects, read operations/events | Frontend (read-only token) | Read-only, benefits from low latency |
| Place/cancel orders | Frontend (advanced) or Backend | Low risk if tightly scoped; latency matters for trading |
| Initiate transfers | Backend | Business logic gating (limits, approvals) |
| Initiate deposits | Backend | Requires builder-side validation |
| Withdrawals | Backend | High risk — fund outflow must be builder-controlled |
| Create/delete objects | Backend | Lifecycle management needs builder orchestration |
Production Readiness Checklist
A step-by-step guide for moving from a demo realm to production. Complete these items before launching your integration to real users.
Demo vs. Production realms: Demo realms use simulated settlement, reset weekly, and are free to use. Production realms connect to live exchanges, process real funds, and never reset.
- Create a production realm. Use
POST /api/v1/realmswithtype: "production"or create one from the Portal. Production realms connect to live venue infrastructure — objects, balances, and operations are persistent and irreversible. - Generate separate production API keys. Never reuse demo API keys in production. Create dedicated keys for each environment and store them securely (environment variables, secrets manager). Rotate keys periodically by creating a new key, migrating, then revoking the old one.
- Scope frontend tokens with minimal permissions. Mint scoped tokens with only the actions your frontend needs — typically
arca:Readand streaming. Keep mutations (transfers, orders, object lifecycle) on your backend using the API key. See Architecture Patterns for the recommended split. - Set short token expiry. Use 15–60 minute token lifetimes. On the client, use
Arca.fromTokenProvider()to automatically refresh tokens before expiry — the SDK calls your provider function and reconnects the WebSocket seamlessly. - Configure builder fees. Set
builderFeeBpson your realm settings for a default fee on all exchange orders, or pass it per-order inplaceOrder(). UsefeeTargetsto split fee revenue to specific paths (e.g. KOL rev-share, referral programs). - Switch from test funding to payment links.
fundAccount()anddefundAccount()are dev/test helpers that mint or burn simulated funds. In production, usecreatePaymentLink()to generate hosted deposit and withdrawal pages for your users. Payment links handle KYC, payment processing, and settlement automatically. - Plan your object path hierarchy. Path structure determines how balances aggregate — changing it later requires migrating objects. Design paths to match your product model (e.g.
/users/{userId}/walletfor payments,/traders/{traderId}/exchangefor trading). See Path Organization. - Implement error handling. Catch
OperationFailedErrorwhen awaiting operation handles and checkerror.operation.failureReason. Implement retries with stable nonces (generate the nonce once, reuse on retries). See Troubleshooting. - Enable real-time monitoring. Use
watchOperations()to track operation lifecycle and detect failures early. UsewatchBalances()to verify settlement completes. For exchange integrations,watchExchangeState()provides live margin and position data. - Complete the Security Pre-flight Checklist. Review every item in the Security Pre-flight Checklist below. It covers API key hygiene, path scoping, scope validation, and token minting safety.
Security Pre-flight Checklist
When exposing Arca to end-users via scoped tokens, the following items remain in the builder's domain. Arca enforces token scope at the API layer, but the builder is responsible for minting correct tokens in the first place.
Each item below represents a security boundary that Arca cannot yet make completely foolproof. Address all of them before shipping scoped-token access to production.
- Never expose API keys on the frontend. API keys are team-scoped with full access to every realm. Always use scoped tokens for end-user-facing code. If an API key leaks, revoke it immediately from the API Keys page.
- Scope tokens to the user's Arca paths. When minting a token, restrict
pathsto the current user's subtree (e.g.,/users/{userId}/*). A token withpaths: ["/*"]gives access to every object in the realm. - Grant minimal permissions. Only include the permissions the user actually needs. Most end users need
readandtransfer— notdeleteorcreate. - Set short expiry times. Frontend tokens should expire in 15–60 minutes. The builder's backend should handle refresh by minting a new token when the current one is close to expiry.
- Validate scope server-side before minting. The builder's backend must verify that the requesting user is entitled to the paths and permissions in the token. Arca trusts the builder to get this right — it has no knowledge of the builder's user model.
- Do not let the frontend request its own scope. The end-user's frontend should not send the desired scope to the builder's backend. The backend should derive the scope from the user's authenticated identity and business rules.
- Use distinct realms for dev and production. Demo realm data is simulated and has permissive defaults. Never use a demo realm for real transactions.
- Treat operation paths as idempotency keys. If a transfer path is reused, the original result is returned (no double-spend). Build unique paths — include a nonce or UUID (e.g.,
/op/transfer/{).userId}-{ uuid} - Monitor via events. Subscribe to realm events (WebSocket or polling) to detect unexpected activity — transfers you did not initiate, objects created outside your expected paths, etc.
Error Handling
Every API error returns the same JSON envelope with a machine-readable code you can match on programmatically and a human-readable message with details.
Response Envelope
Successful responses:
{ "success": true, "data": { ... }}Error responses:
{ "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Realm name must be 100 characters or fewer", "errorId": "err_a1b2c3d4" }}The errorId field is a unique identifier for the error occurrence. It is always present on 5xx errors and on most 4xx errors. When contacting support about an error, include the errorId so we can locate the exact request in our logs.
Validation Errors (400)
| Code | Description |
|---|---|
VALIDATION_ERROR | General input validation failed (missing fields, format errors, constraint violations) |
MISSING_PARAM | A required parameter was not provided |
INVALID_AMOUNT | Amount is not a valid number, is negative, or exceeds limits |
INVALID_PATH | Object path violates naming rules (reserved prefix, invalid characters, parent conflict) |
INVALID_TYPE | Object type is not one of the allowed types |
INVALID_REQUEST | Request body is malformed or contains contradictory parameters |
Business Logic Errors (400)
| Code | Description |
|---|---|
INSUFFICIENT_BALANCE | The source object does not have enough settled balance for this transfer or order |
OBJECT_NOT_READY | The object exists but is not yet in a state where this operation is allowed |
DELETION_BLOCKED | Object cannot be deleted (non-zero balance, in-flight operations, or active exchange positions) |
ORDER_FAILED | The exchange rejected the order (insufficient margin, invalid size, or market-specific constraints) |
MARKET_DATA_UNAVAILABLE | Price data for the requested market is not currently available |
DIRECT_EXCHANGE_TRANSFER_NOT_ALLOWED | Transfers directly between two exchange objects are not supported; route through a denominated object |
SIGNIN_FAILED | Invalid email or password |
Authentication Errors (401)
| Code | Description |
|---|---|
UNAUTHENTICATED | No valid credential provided, or the token/API key has expired or been revoked |
Authorization Errors (403)
| Code | Description |
|---|---|
FORBIDDEN | The token scope or role does not permit this action on this resource |
ADMIN_REQUIRED | This endpoint requires admin-level access |
INSUFFICIENT_ROLE | Your org role is below the minimum required for this action (e.g., Developer trying an Admin-only operation) |
REALM_SCOPE_MISMATCH | The scoped token or API key does not have access to the requested realm |
Not Found Errors (404)
| Code | Description |
|---|---|
NOT_FOUND | Generic resource not found |
REALM_NOT_FOUND | No realm with the specified ID or slug |
OBJECT_NOT_FOUND | No Arca object with the specified ID or path in this realm |
OPERATION_NOT_FOUND | No operation with the specified ID |
ORDER_NOT_FOUND | No order with the specified ID on this exchange |
USER_NOT_FOUND | No user account with the specified identifier |
ORG_NOT_FOUND | No organization with the specified ID |
MEMBER_NOT_FOUND | No member with the specified ID in this organization |
INVITATION_NOT_FOUND | No pending invitation with the specified ID |
ACCOUNT_NOT_FOUND | No exchange account linked to this object |
PROFILE_NOT_FOUND | No user profile found for the authenticated user |
PAYMENT_LINK_NOT_FOUND | No payment link with the specified ID |
Conflict Errors (409)
| Code | Description |
|---|---|
CONFLICT | Generic conflict (duplicate resource or invalid state transition) |
ALREADY_EXISTS | A resource with this identifier already exists (realm slug, object path, API key name) |
ALREADY_MEMBER | The invited user is already a member of this organization |
ALREADY_DELETED | The resource has already been deleted |
IDEMPOTENCY_VIOLATION | The operation path was reused with different parameters (same path must have same inputs) |
INVITATION_EXPIRED | The invitation has passed its expiration window |
INVITATION_PROCESSED | The invitation has already been accepted or rejected |
INVITATION_REVOKED | The invitation was revoked by an admin before it could be accepted |
PAYMENT_LINK_EXPIRED | The payment link has passed its expiration time |
PAYMENT_LINK_COMPLETED | The payment link has already been used |
Server Errors (5xx)
| Code | HTTP Status | Description |
|---|---|---|
INTERNAL_ERROR | 500 | Unexpected server error |
EXCHANGE_ERROR | 502 | The upstream exchange returned an error |
EXCHANGE_PROVISION_FAILED | 502 | Failed to provision an exchange account for this object |
TEMPORAL_ERROR | 502 | Failed to start or signal the background workflow for this operation |
EXCHANGE_UNAVAILABLE | 503 | The exchange is temporarily unreachable |
SERVICE_UNAVAILABLE | 503 | An internal service is temporarily unreachable |
message field on 500 and 502 errors is replaced with a generic string. Match on the code field for programmatic handling, and include the errorId when contacting support.Error Handling in the SDK
The TypeScript SDK throws typed errors you can catch and inspect:
import Arca, { ArcaError, OperationFailedError } from '@arca/sdk';
const arca = new Arca({ apiKey: 'arca_...' });
try { const { nonce } = await arca.nonce('/transfers/payment-001'); await arca.transfer({ from: walletId, to: recipientId, amount: '100.00', nonce, });} catch (err) { if (err instanceof OperationFailedError) { // Operation was created but failed during execution console.error('Operation failed:', err.operation.failureMessage); } else if (err instanceof ArcaError) { // API-level error — match on err.code switch (err.code) { case 'INSUFFICIENT_BALANCE': console.error('Not enough funds:', err.message); break; case 'IDEMPOTENCY_VIOLATION': console.error('Path reused with different params'); break; default: console.error(`[${err.code}] ${err.message}`); } }}Troubleshooting
Common problems organized by scenario. For the full error code catalog, see the Error Handling section above.
First-Hour Mistakes
The five most common issues during initial integration:
| Symptom | Cause | Fix |
|---|---|---|
REALM_NOT_FOUND on every call | Missing await arca.ready() before using SDK methods | Always call await arca.ready() after constructing the Arca client |
IDEMPOTENCY_VIOLATION | Calling nonce() inside a retry loop (generates a new key each time) | Generate the nonce once, then reuse the same nonce on retries |
| Object already exists but has different type | Calling ensureDenominatedArca() at a path that has a different object type | Use a different path, or delete the existing object first with ensureDeleted() |
INVALID_COIN on market data calls | Using bare symbols ("BTC") instead of canonical IDs ("hl:BTC") | Always use the full canonical format: "hl:BTC", "hl:1:TSLA" |
| WebSocket events not arriving | Not calling watchOperations() or watchBalances() before expecting events | Subscribe to the relevant watch stream before performing operations |
Scoped Token Debugging
When a scoped token request is rejected with FORBIDDEN, the error message says which action was denied but does not reveal the full policy. Here's how to debug:
- Check the error response. The
messagefield includes the denied action (e.g."action arca:Transfer not allowed"). - Decode the token. Scoped tokens are JWTs. Paste yours into a JWT debugger to see the
scopeclaim — it lists every (action, resource) pair the token grants. - Compare action names. The denied action must exactly match one of the scope entries. Common mismatches: using
arca:Writewhen the endpoint requiresarca:Transfer, or scoping to/users/alice/*when the request targets/users/bob/*. - Check path scoping. If the scope grants
arca:Readon/users/alice/*, the token can read Alice's objects but not Bob's. Broaden the path to/users/*if the client needs access to multiple users.
Settlement Failure Diagnosis
When an operation reaches a failed terminal state, thefailureReason field explains why:
| Failure Reason | What happened | Recovery |
|---|---|---|
INSUFFICIENT_BALANCE | Source account didn't have enough settled funds at execution time | Verify balances before the transfer; for exchange operations, check margin |
EXCHANGE_REJECTED | The exchange venue rejected the order (margin, leverage, or market condition) | Check failureMessage for the venue's specific rejection reason |
EXPIRED | The settlement window elapsed before the operation could complete | Retry with a new nonce if the underlying condition is resolved |
OBJECT_NOT_FOUND | The source or destination object was deleted during settlement | Verify both objects exist before retrying |
Failed operations are permanent — they cannot be retried directly. Create a new operation with a new nonce to retry the intent. For transfers that fail after PNR (point of no return), Arca automatically creates a compensating transaction to reverse the source debit.
Exchange Order Failures
| Scenario | Cause | Recovery |
|---|---|---|
| Order rejected: insufficient margin | Exchange equity minus existing positions doesn't cover the new order's margin requirement | Reduce order size, increase leverage (if appropriate), or fund the exchange object |
| Order rejected: leverage mismatch | Requested leverage differs from the active leverage for that coin | Call updateLeverage() first, then place the order |
| Max-size order rejected | Max order size dropped more than the maxSizeTolerance (default 2%) between display and execution | Refresh the displayed max and let the user confirm again; increase tolerance if appropriate |
| Order fills at unexpected price | Market orders execute at the current market price, which may differ from the displayed mid price | Use limit orders for price-sensitive trades; display the order book spread to users |
When to Contact Arca Support
Most issues are resolvable using the error codes and troubleshooting steps above. Contact Arca support (support@arcaos.io) when:
- You receive a
5xxerror that persists across retries — include theerrorId - An operation is stuck in
processingstate for more than 5 minutes - Balance totals don't match what you expect after all operations have settled
- The WebSocket connection fails to reconnect after multiple attempts
- You need help designing your path hierarchy or permission model