Financial infrastructure for autonomous software.
Agents submit structured intents over HTTPS. VEYRA checks them against owner policies, simulates on Solana mainnet and signs only within on-chain limits. Opaque transactions are never accepted.
For AI agents
An AI agent can open an account, fund it, configure itself and operate without a browser, a wallet app or a human click. It holds an ed25519 owner key in its own runtime and proves control of it by signing. VEYRA never receives the key: every on-chain step comes back as a transaction prepared and simulated on Solana mainnet, which the agent signs and submits.
| Resource | For |
|---|---|
| /openapi.json | OpenAPI 3.1 spec with schemas, units and examples. Load it as tools. |
| /llms.txt | The whole guide as plain text for a model's context, including TypeScript and Python. |
| /sdk/veyra.ts | Single-file TypeScript SDK with no dependencies. Download it with curl. |
POST /api/v1/intents/preview, which evaluates every policy and quotes fees without creating or signing anything. To keep a person in control, add their wallet to coOwners and set threshold to 2.# 1. Prove control of the owner key (ed25519). Nothing else is needed: no browser, no wallet app.curl -X POST "$VEYRA_URL/api/v1/auth/challenge" -H "Content-Type: application/json" -d '{ "address": "<owner public key, base58>" }'# -> { "message": "...", "nonceToken": "..." } sign the UTF-8 bytes of message, base58 the signaturecurl -X POST "$VEYRA_URL/api/v1/auth/token" -H "Content-Type: application/json" -d '{ "address": "...", "message": "...", "signature": "...", "nonceToken": "...", "organizationName": "My agent" }'# -> { "token": "vot_..." } the organization exists now# 2. Create the Squads account. The response is a transaction prepared and simulated on mainnet.curl -X POST "$VEYRA_URL/api/v1/accounts" -H "Authorization: Bearer $VOT" -H "Content-Type: application/json" -d '{ "name": "Agent Ops", "type": "AGENT", "coOwners": [], "threshold": 1, "vaults": [{ "name": "Operating Vault", "type": "OPERATING", "description": "" }], "deposit": { "asset": "SOL", "amount": "0.05" } }'# -> { "id": "ptx_...", "transaction": "<base64>", "simulation": { "ok": true } }# 3. Add the owner signature to the transaction (keep existing signatures) and submit it.curl -X POST "$VEYRA_URL/api/v1/tx/ptx_.../submit" -H "Authorization: Bearer $VOT" -H "Content-Type: application/json" -d '{ "transaction": "<signed base64>" }'# -> { "signature": "...", "result": { "accountId": "acc_..." } } confirmed on Solana mainnet# 4. Same pattern for POST /api/v1/agents, deposits, approvals and withdrawals.// curl -o veyra.ts "$VEYRA_URL/sdk/veyra.ts" (single file, no dependencies)import { Veyra, signInWithKey, type OwnerSigner } from "./veyra";import { ed25519 } from "@noble/curves/ed25519.js";import bs58 from "bs58";const secret = ed25519.utils.randomSecretKey(); // persist securely, then fund the address with SOLconst owner: OwnerSigner = { publicKey: bs58.encode(ed25519.getPublicKey(secret)), signMessage: (m) => ed25519.sign(m, secret) };const { token } = await signInWithKey(process.env.VEYRA_URL!, owner, { organizationName: "My agent" });const veyra = new Veyra({ apiKey: token, baseUrl: process.env.VEYRA_URL! });const { result: { accountId } } = await veyra.owner.createAccount({ name: "Agent Ops", deposit: { asset: "SOL", amount: "0.05" } }, owner);const ctx: any = await veyra.context();const payee = await veyra.owner.createCounterparty({ name: "Vendor", type: "BUSINESS", address: "<base58>" });const { result: { agentId } } = await veyra.owner.createAgent( { accountId, vaultId: ctx.vaults[0].id, name: "Atlas", kind: "PAYMENT", limits: [{ asset: "SOL", amount: "0.02" }], period: "DAY", counterpartyIds: [payee.id], gasSol: "0.01" }, owner,);const { execution } = await veyra.intents.submit({ agentId, action: "PAYMENT", asset: "SOL", amount: "0.003", counterpartyId: payee.id });console.log(await veyra.executions.wait(execution.id));Already have credentials? GET /api/v1/context returns your agent id, vault ids, allowed actions, remaining on-chain allowance and the counterparties you can pay, in one call.
Units
| Field | Unit | Example |
|---|---|---|
Request amount (intents, payments, deposits, limits) | Decimal string in token units | "12.5" USDC |
Balances, intent.amount, limits, simulation deltas | Integer string in the smallest unit (USDC 6 decimals, SOL 9) | "12500000" = 12.5 USDC |
amountUsd, policy amounts, allowances | Integer USD cents | 250000 = $2,500.00 |
Fees (feeQuote, preview fee) | Integer string micro-USD (1e-6) | "1879000" = $1.879 |
maxSlippageBps | Basis points | 50 = 0.50% |
Quickstart
The base URL is the origin of your deployment. Every request carries a bearer credential. Both kinds are created in the developer dashboard, shown once and stored by VEYRA only as SHA-256 hashes.
| Credential | Format | Scope |
|---|---|---|
| API key | Bearer vk_live_... | Organization. Read, submit intents and payments, pause agents, manage counterparties and webhooks. Cannot vote on approvals or change policies. |
| Agent session | Bearer vas_... | One agent: its vaults, allowed actions, a USD spending allowance and an expiry. Can submit intents, read its own state and request changes. |
# The base URL is the origin of your VEYRA deploymentexport VEYRA_URL="https://your-veyra-deployment"# API key (organization scope), created in /developers/dashboardexport VEYRA_API_KEY="vk_live_..."# Agent session token (one agent, its vaults and allowed actions)export VEYRA_AGENT_TOKEN="vas_..."# Find your agentId, vaultId and counterparty ids with one callcurl "$VEYRA_URL/api/v1/context" -H "Authorization: Bearer $VEYRA_AGENT_TOKEN"curl -X POST "$VEYRA_URL/api/v1/intents/preview" \ -H "Authorization: Bearer $VEYRA_AGENT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "agentId": "agt_...", "action": "SWAP", "asset": "USDC", "outputAsset": "SOL", "amount": "250", "maxSlippageBps": 50 }'curl -X POST "$VEYRA_URL/api/v1/intents" \ -H "Authorization: Bearer $VEYRA_AGENT_TOKEN" \ -H "Content-Type: application/json" \ -H "idempotency-key: rebalance-2026-10-05-001" \ -d '{ "agentId": "agt_...", "action": "SWAP", "asset": "USDC", "outputAsset": "SOL", "amount": "250", "maxSlippageBps": 50, "justification": "Rebalance to target SOL weight" }'# 202 Accepted (200 with "replayed": true for a repeated idempotency key)# { "execution": { "id": "exe_...", "state": "CREATED", ... }, "replayed": false }Poll until the state is terminal or AWAITING_APPROVAL, or subscribe to webhooks.
curl "$VEYRA_URL/api/v1/executions/exe_..." \ -H "Authorization: Bearer $VEYRA_AGENT_TOKEN"Agent Accounts
An account is a Squads v4 multisig on Solana mainnet. Owners are wallets with full permissions; the threshold sets how many must approve. Each agent receives a dedicated executor key, derived server-side and never stored, added to the multisig with Initiate and Execute permissions only. It cannot vote.
Creating an agent, changing its spending limits and revoking it are Squads transactions signed by owner wallets, so they happen in the app. Through the API you read agents and pause or resume them.
curl "$VEYRA_URL/api/v1/agents/agt_..." \ -H "Authorization: Bearer $VEYRA_AGENT_TOKEN"# {# "agent": { "id": "agt_...", "status": "ACTIVE", "chainStatus": "ACTIVE",# "executor": "<executor public key>", "primaryVaultId": "vlt_...", ... },# "limits": [ { "asset": "USDC", "amount": "...", "period": "DAY",# "destinations": ["..."], "remaining": "...", ... } ],# "wallet": { "balances": [ ... ], "totalUsd": 0 },# "gasLamports": 0# }# Pause or resume an agent (API key)curl -X PATCH "$VEYRA_URL/api/v1/agents/agt_..." \ -H "Authorization: Bearer $VEYRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "PAUSED" }'Vaults
A vault is a Squads vault PDA of an account. Funds stay in the vault under owner control. Agents draw from it only through their on-chain spending limits (amount per period, per asset, to allow-listed destinations) or through owner-approved proposals.
curl "$VEYRA_URL/api/v1/vaults" \ -H "Authorization: Bearer $VEYRA_API_KEY"# [ { "id": "vlt_...", "name": "...", "type": "PAYMENT", "address": "<vault PDA>",# "status": "ACTIVE", "balances": [ { "asset": "USDC", "amount": "<base units>",# "decimals": 6, "usdValue": 0 } ], "totalUsd": 0, ... } ]Token amounts are base-unit strings. USD values are integer cents. Vaults are created, funded and frozen by owners in the app. A frozen vault blocks every intent that draws from it.
Policies
Policies are deterministic rules evaluated before anything is signed. There is no model in the loop: the same intent and state always produce the same decision. USD amounts are cents. Empty scopes apply to every agent and vault in the organization.
| Rule kind | Parameters |
|---|---|
DAILY_LIMIT | amountUsd |
MAX_TRANSACTION | amountUsd |
APPROVAL_THRESHOLD | amountUsd |
ALLOWED_ASSETS / BLOCKED_ASSETS | assets |
ALLOWED_PROTOCOLS / BLOCKED_PROTOCOLS | protocols |
ALLOWED_COUNTERPARTIES / BLOCKED_COUNTERPARTIES | counterpartyIds |
ALLOWED_ACTIONS | actions |
TIME_WINDOW | startHourUtc, endHourUtc, days |
EXECUTION_FREQUENCY | maxPerHour |
MAX_SLIPPAGE | bps |
COUNTERPARTY_DAILY_LIMIT | asset, amountUsd |
Every evaluation returns a decision object. It is attached to the execution as policy and returned by the preview endpoint as evaluation.
{ "decision": "REQUIRE_APPROVAL", "triggeredPolicies": [ { "policyId": "pol_...", "reason": "TRANSACTION_ABOVE_THRESHOLD", "outcome": "REQUIRE_APPROVAL", "message": "..." } ], "approvalRequirement": { "rule": { "kind": "THRESHOLD", "required": 1, "of": ["OWNER"] }, "approvalsRequired": 1 }, "evaluatedPolicyIds": ["pol_..."], "evaluatedAt": "<ISO 8601>"}ALLOWexecutes through the agent's spending limit.REQUIRE_APPROVALwaits for owners. For vault funds the agent proposes a Squads transaction that owners approve on-chain.BLOCKstops the execution. Nothing is signed.
Policies are changed by owners. Agents propose changes through change requests (see Approvals).
Execution
| Action | What it does | Requires |
|---|---|---|
SWAP | Swap from the agent's working wallet, routed through Jupiter. | outputAsset, maxSlippageBps |
PAYMENT | Pay a counterparty from a vault. | counterpartyId |
TRANSFER | Move value from a vault to a counterparty. | counterpartyId |
ALLOCATE | Move capital from a vault to the agent's working wallet. | No extra field |
RETURN | Return capital from the working wallet to the vault. | No extra field |
Body fields: agentId, vaultId (optional, defaults to the agent's primary vault), action, asset, amount (decimal token amount as a string), and optionally outputAsset, counterpartyId, maxSlippageBps (0 to 5000), memo, justification.
{ "amountUsd": 25000, // cents, priced server-side "evaluation": { "decision": "ALLOW", "triggeredPolicies": [], ... }, "fee": { // USD micro-units (1e-6), as strings "networkFee": "...", "executionFee": "...", "providerFee": "...", "totalFee": "..." }, "route": "JUPITER", // SPENDING_LIMIT | SQUADS_PROPOSAL | AGENT_WALLET | JUPITER "estimatedTime": "About 15 seconds"}Execution states
CREATEDIntent recorded with its idempotency key.VALIDATINGAgent, vault, session scope and balance checks.POLICY_CHECKDeterministic policy evaluation.AWAITING_APPROVALPolicy requires owners to approve. Execution resumes after the decision.APPROVEDAllowed by policy, or approved by owners.SIMULATINGTransaction built and simulated against mainnet, then compared with the intent.READYSimulation matched the intent. Ready to sign.SUBMITTEDSigned and sent to Solana.CONFIRMEDConfirmed on-chain.SETTLEDFinal. Settlement recorded with signature, slot and fee.BLOCKEDFinal. Policy, simulation mismatch or rejection. Nothing was signed.FAILEDFinal. Check error and fundsMayHaveMoved before retrying.CANCELLEDFinal. Cancelled before submission.idempotency-key header (8 to 128 characters). The same key with the same body returns the original execution with replayed: true. The same key with a different body returns 409 IDEMPOTENCY_CONFLICT. Retry network failures with the same key.Payments
A payment is a PAYMENT intent from a vault to a registered counterparty, with optional reference, invoice and memo. Payable assets: USDC, USDT, PYUSD, SOL. The destination must be on the agent's on-chain allow-list.
curl -X POST "$VEYRA_URL/api/v1/payments" \ -H "Authorization: Bearer $VEYRA_API_KEY" \ -H "Content-Type: application/json" \ -H "idempotency-key: inv-0042-payment" \ -d '{ "agentId": "agt_...", "counterpartyId": "cp_...", "asset": "USDC", "amount": "980.00", "reference": "PO-118", "invoice": "INV-0042", "memo": "October hosting" }'# { "paymentId": "pay_...", "execution": { "id": "exe_...", "state": "CREATED", ... } }/api/v1/payments requires an API key. Agent session tokens pay by submitting a PAYMENT intent to /api/v1/intents.Approvals
Approvals are decided by organization members signing with their wallets (owners by default), never by API keys or agents. For vault funds, approval is an on-chain Squads proposal vote. For swaps from an agent's working wallet and for change requests, owners sign an off-chain approval message with their wallet.
curl "$VEYRA_URL/api/v1/approvals?status=PENDING" \ -H "Authorization: Bearer $VEYRA_API_KEY"Agents ask for changes instead of working around limits. Types: POLICY_CHANGE, COUNTERPARTY, PROTOCOL, LIMIT_INCREASE, AGENT_PERMISSION.
curl -X POST "$VEYRA_URL/api/v1/agents/agt_.../requests" \ -H "Authorization: Bearer $VEYRA_AGENT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "LIMIT_INCREASE", "title": "Raise daily limit", "requestedChange": "Daily limit to 5,000 USDC", "currentRule": "Daily limit 2,000 USDC", "justification": "Settlement volume doubled this week." }'# Returns the created approval request (status PENDING) for owners to decide.SDK
The SDK is one dependency-free TypeScript file for Node 18+, Bun, Deno, browsers and edge runtimes. Download it from /sdk/veyra.ts (curl -o veyra.ts $VEYRA_URL/sdk/veyra.ts). It is not on npm, so there is nothing to install.
// curl -o veyra.ts "$VEYRA_URL/sdk/veyra.ts"import { Veyra, VeyraError } from "./veyra";const veyra = new Veyra({ apiKey: process.env.VEYRA_AGENT_TOKEN!, // vk_live_... or vas_... baseUrl: process.env.VEYRA_URL!,});const agent = veyra.agent("agt_...");// Check the decision and fee first. Nothing is created or signed.const preview = await veyra.intents.preview({ agentId: agent.id, action: "PAYMENT", asset: "USDC", amount: "120", counterpartyId: "cp_...",});if (preview.evaluation.decision !== "BLOCK") { const { execution } = await agent.pay( { counterpartyId: "cp_...", asset: "USDC", amount: "120", memo: "INV-0042" }, { idempotencyKey: "inv-0042" }, ); const final = await veyra.executions.wait(execution.id, { timeoutMs: 90_000 }); // SETTLED | BLOCKED | FAILED | CANCELLED | AWAITING_APPROVAL}try { await agent.swap({ from: "USDC", to: "SOL", amount: "250", maxSlippageBps: 50 });} catch (e) { if (e instanceof VeyraError) console.error(e.status, e.code, e.message);}new Veyra({ apiKey, baseUrl })veyra.intents.submit(body, { idempotencyKey? })veyra.intents.preview(body)veyra.executions.get(id)veyra.executions.list({ agentId?, state?, limit? })veyra.executions.wait(id, { timeoutMs?, intervalMs? })veyra.payments.create(body, { idempotencyKey? }) // API keyveyra.approvals.list(status?)veyra.agents.list()veyra.agents.get(id)veyra.agents.requestChange(agentId, body)veyra.webhooks.verify(rawBody, header, secret) // async, Web Cryptoveyra.context() // ids, scope, limits, unitssignInWithKey(baseUrl, ownerSigner) // -> { token: "vot_..." }veyra.owner.createAccount(body, signer) // prepare, sign, submit, confirmveyra.owner.deposit(vaultId, body, signer)veyra.owner.createCounterparty(body)veyra.owner.createAgent(body, signer)veyra.owner.issueSession(body) // -> { token: "vas_..." }veyra.owner.approve(approvalId, signer)veyra.owner.withdraw(vaultId, body, signer)veyra.owner.run(path, body, signer) // any endpoint returning a prepared transactionsignPreparedTransaction(base64, signer) // adds one signature, keeps the restconst agent = veyra.agent(agentId)agent.swap({ from, to, amount, maxSlippageBps })agent.pay({ counterpartyId, asset, amount, memo })agent.allocate({ asset, amount })agent.returnCapital({ asset, amount })agent.request({ type, title, requestedChange, justification })API
JSON over HTTPS. Errors return { "error": { "code", "message" } }. Agent session tokens only see their own agent's executions, approvals and detail.
| Method | Path | Description | Credential |
|---|---|---|---|
| POST | /api/v1/auth/challenge | Start sign-in for an owner key. Public. | None |
| POST | /api/v1/auth/token | Signed challenge for an owner token (vot_...). Public. | None |
| GET | /api/v1/context | Ids, scope, on-chain limits, units and rate limits for this credential. | Any |
| POST | /api/v1/tx/:id/submit | Submit an owner-signed prepared transaction. | Owner |
| POST | /api/v1/accounts | Create a Squads account. Returns a prepared transaction. | Owner |
| POST | /api/v1/agents | Create and authorize an agent. Returns a prepared transaction. | Owner |
| POST | /api/v1/vaults/:id/deposit | Deposit from the owner wallet. Prepared transaction. | Owner |
| POST | /api/v1/vaults/:id/withdraw | Owner withdrawal, step 1 of 2. Prepared transaction. | Owner |
| POST | /api/v1/approvals/:id/onchain-vote | Owner vote on a Squads proposal. Prepared transaction. | Owner |
| POST | /api/v1/developers/sessions | Issue an agent session token (vas_...). | Owner |
| POST | /api/v1/intents | Submit an intent. Header idempotency-key. | Key, Agent |
| POST | /api/v1/intents/preview | Policy decision, fee quote and route. Read-only. | Key, Agent |
| GET | /api/v1/executions | List executions. ?agentId= &state= &limit= | Key, Agent |
| GET | /api/v1/executions/:id | One execution with steps, policy, simulation, signature. | Key, Agent |
| POST | /api/v1/payments | Create a payment to a counterparty. | Key |
| GET | /api/v1/payments | List payments. | Key |
| GET | /api/v1/approvals | List approvals. ?status=PENDING | Key, Agent |
| POST | /api/v1/agents/:id/requests | Agent change request for owners to decide. | Key, Agent |
| GET | /api/v1/agents | List agents. | Key, Agent |
| GET | /api/v1/agents/:id | Agent, on-chain limits, working wallet. | Key, Agent |
| PATCH | /api/v1/agents/:id | Pause or resume: { status }. | Key |
| GET | /api/v1/vaults | Vaults with live balances. | Key |
| GET | /api/v1/policies | Policies. | Key |
| GET | /api/v1/counterparties | Counterparties. | Key |
| POST | /api/v1/webhooks | Create an endpoint. Returns the secret once. | Key |
| PATCH | /api/v1/webhooks/:id | Enable or disable: { enabled }. | Key |
| GET | /api/v1/usage | Usage and fees, last 30 days. | Key |
Errors
| Status | Code | Meaning |
|---|---|---|
400 | VALIDATION_ERROR | Body failed validation. The message lists each field. |
401 | UNAUTHORIZED | Missing, invalid, revoked or expired credential. |
403 | FORBIDDEN | Credential lacks permission, or the session scope does not allow the intent. |
404 | NOT_FOUND | Resource does not exist in your organization. |
409 | IDEMPOTENCY_CONFLICT | Idempotency key reused with a different body. |
429 | RATE_LIMITED | Too many requests. Retry-After header and error.retryAfterSeconds give the delay. |
409 | TX_TAMPERED | The submitted transaction differs from the prepared one. Nothing was sent. |
503 | PRICE_UNAVAILABLE | USD pricing unavailable. Nothing was created. |
Rate limits
| Scope | Limit | Notes |
|---|---|---|
| Owner token / signed-in user | 120 per minute | Per member. |
| API key | 600 per minute | Per key. |
| Agent session | Configured per token | Default 30 per minute, set when the token is issued. |
| Organization | 2,000 per minute | Across all credentials. |
| Loop protection | 12 actions per minute, 3 identical intents per 5 minutes, 500 per day | Per agent. Tripping it blocks the intent and pauses the agent until an owner resumes it. |
429 responses carry a Retry-After header and error.retryAfterSeconds. Full machine-readable description: /openapi.json.
Webhooks
Endpoints must be public HTTPS URLs. Each delivery is a POST with a JSON body, signed with your endpoint secret. Failed deliveries are retried twice (after 1 and 4 seconds) with an 8 second timeout per attempt. Delivery results appear in the developer dashboard.
curl -X POST "$VEYRA_URL/api/v1/webhooks" \ -H "Authorization: Bearer $VEYRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/veyra/webhooks", "events": ["agent.execution.settled", "agent.execution.blocked", "approval.required"] }'# { "id": "whk_...", "secret": "whsec_..." } The secret is shown once.agent.execution.requestedAn intent was submitted.agent.execution.approvedOwners approved an execution.agent.execution.blockedPolicy or simulation blocked an execution.agent.execution.settledAn execution settled on Solana.payment.createdA payment was created.payment.settledA payment settled.approval.requiredAn approval request needs owners.approval.completedAn approval request was decided.policy.changedA policy was created or changed.vault.fundedA vault received a deposit.vault.frozenA vault was frozen.Signature
Header veyra-signature: t=<unix>,v1=<hex hmac_sha256(secret, "<t>.<body>")>. Compute the HMAC over the timestamp, a period and the raw body, compare in constant time and reject timestamps more than 300 seconds from now.
POST /veyra/webhookscontent-type: application/jsonuser-agent: VEYRA-Webhooks/1veyra-signature: t=1791200000,v1=5f2c...e81a{ "id": "whd_...", "type": "agent.execution.settled", "createdAt": "<ISO 8601>", "data": { ... } }import { createHmac, timingSafeEqual } from "node:crypto";const TOLERANCE_S = 300;export function verifyVeyraSignature(rawBody: string, header: string | null, secret: string): boolean { if (!header) return false; const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2) as [string, string])); const t = Number(parts.t); if (!Number.isInteger(t) || !parts.v1) return false; // Reject old or future timestamps (replay protection) if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE_S) return false; const expected = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"), "hex"); const received = Buffer.from(parts.v1, "hex"); return expected.length === received.length && timingSafeEqual(expected, received);}// Next.js route handlerexport async function POST(req: Request) { const rawBody = await req.text(); // verify the exact bytes you received if (!verifyVeyraSignature(rawBody, req.headers.get("veyra-signature"), process.env.VEYRA_WEBHOOK_SECRET!)) { return new Response("invalid signature", { status: 400 }); } const event = JSON.parse(rawBody) as { id: string; type: string; data: Record<string, unknown> }; // Deduplicate on event.id: deliveries can be retried. return new Response("ok");}With the SDK: await veyra.webhooks.verify(rawBody, header, secret) returns { ok: true } or { ok: false, reason }.