Official JavaScript/TypeScript SDK for the OCTA API.
- @octaspace/sdk
npm install @octaspace/sdk
# or
pnpm add @octaspace/sdk
# or
yarn add @octaspace/sdkimport { OctaClient } from '@octaspace/sdk'
const client = new OctaClient({
apiKey: process.env.OCTA_API_KEY,
})
// List available machines for rent
const machines = await client.services.mr.available()
console.log(`${machines.length} machines available`)
// Start a session
const { uuid } = await client.services.mr.start({
node_id: machines[0].node_id,
disk_size: 50,
image: 'ubuntu:22.04',
ports: [22],
})
// Check session status
const info = await client.services.session(uuid).info()
console.log('Session ready:', info.is_ready)
console.log('SSH:', info.ssh_direct ? `${info.ssh_direct.host}:${info.ssh_direct.port}` : '—')
// Stop when done
await client.services.session(uuid).stop({ score: 5 })Authenticated API requests require an API key passed in the Authorization header.
The public network.get() endpoint works without a key. The SDK adds the header
automatically only for protected endpoints.
const client = new OctaClient({
apiKey: 'your_api_key_here',
})For public network stats only:
const client = new OctaClient({})
const stats = await client.network.get()Read-only methods also accept optional request overrides for UI integrations:
const controller = new AbortController()
const nodes = await client.nodes.list({ signal: controller.signal, retries: 0 })Best practice: load the key from an environment variable, never hardcode it.
// Node.js / server-side
const client = new OctaClient({
apiKey: process.env.OCTA_API_KEY,
})Browser warning: Do not use your API key in client-side browser code — it will be visible to all users. Use a server-side proxy instead. See Browser Usage.
// Full account details
const account = await client.accounts.get()
// account.email, account.uid, account.wallet, account.balance (Wei), account.ref_code
// Balance only (lighter request)
const { balance } = await client.accounts.balance()
console.log(`Balance: ${balance} Wei`)
// Generate a new deposit wallet address
const { address } = await client.accounts.generateWallet()Public endpoint — no API key required.
const stats = await client.network.get()
console.log(`Market price: $${stats.market_price}`)
console.log(`Total nodes: ${stats.nodes.count}`)
console.log(`Monthly staking ROI: ${stats.mmROI}%`)
console.log(`Blockchain height: ${stats.blockchain.height}`)// List available pre-configured applications (Docker images)
const apps = await client.apps.list()
for (const app of apps) {
console.log(`${app.name} (${app.category}) — ${app.image}`)
}
// Find an app by name
const stableApp = apps.find((a) => a.name === 'Stable Diffusion')This resource is for node operators managing their own machines.
// List your nodes
const nodes = await client.nodes.list()
// Get full info for a specific node
const node = await client.nodes.get('node_id')
console.log(`State: ${node.state}`) // 'idle' | 'busy'
console.log(`CPU load: ${node.data.cpu_load_percent}%`)
console.log(`Location: ${node.location.city}, ${node.location.country}`)
console.log(`GPUs: ${node.data.gpus.map((g) => g.model).join(', ')}`)
// Update pricing (amounts in Wei)
await client.nodes.updatePrices('node_id', {
base: 50_000_000_000_000_000, // ~$0.05/hr (depends on OCTA/USD rate)
storage: 5_000_000_000_000_000,
traffic: 2_000_000_000_000_000,
})
// Reboot a node
await client.nodes.reboot('node_id')
// Download the node ident file (returns Blob)
const identBlob = await client.nodes.downloadIdent('node_id')
// Download node logs (returns Blob)
const logsBlob = await client.nodes.downloadLogs('node_id')Saving blobs — Node.js:
import { writeFile } from 'node:fs/promises'
const identBlob = await client.nodes.downloadIdent('node_id')
await writeFile('node.ident', Buffer.from(await identBlob.arrayBuffer()))Saving blobs — Browser:
const logsBlob = await client.nodes.downloadLogs('node_id')
const url = URL.createObjectURL(logsBlob)
const a = document.createElement('a')
a.href = url
a.download = 'node.log'
a.click()
URL.revokeObjectURL(url)Rent a machine to run any Docker container.
// Browse available machines
const machines = await client.services.mr.available()
// Filter by GPU availability
const gpuMachines = machines.filter((m) => m.is_has_gpu)
// Sort by price
const cheapest = machines.sort((a, b) => a.total_price_usd - b.total_price_usd)
console.log(`${machines[0].cpu_cores} cores, ${machines[0].gpus[0]?.model}`)
console.log(`Price: $${machines[0].total_price_usd.toFixed(4)}/hr`)
// Start a session with a public Docker image
const { uuid } = await client.services.mr.start({
node_id: machines[0].node_id,
disk_size: 50, // GB
image: 'ubuntu:22.04',
ports: [22, 3000], // TCP/UDP ports to expose
http_ports: [8080], // HTTP ports (get a public URL)
envs: { MY_VAR: 'hello' },
})
// Start a session with a pre-configured OCTA app
// image must match the app's own image field
const { uuid: uuid2 } = await client.services.mr.start({
node_id: machines[0].node_id,
disk_size: 100,
image: 'ubuntu:24.04', // required — use the app's image value
app: 'app-uuid-from-apps-list',
})Run distributed Blender / rendering workloads.
// Browse available render nodes
const renderNodes = await client.services.render.available()
// Find the node with the best Blender score
const best = renderNodes.sort((a, b) => b.blender_score - a.blender_score)[0]
console.log(`Best Blender score: ${best.blender_score}`)
console.log(`AI score: ${best.ai_score}`)
// Start a render session
const { uuid } = await client.services.render.start({
node_id: best.node_id,
disk_size: 200,
'multi-gpu-worker': true, // one worker per GPU
envs: { PROJECT_ID: 'my-project' },
})// Browse available VPN nodes
const vpnNodes = await client.services.vpn.available()
// Filter residential IPs
const residential = vpnNodes.filter((n) => n.residential)
// Find by country
const nlNode = vpnNodes.find((n) => n.country_iso === 'NL')
console.log(`Price: $${nlNode?.traffic_price_usd}/GB`)
// Start a WireGuard VPN session (default)
const { uuid } = await client.services.vpn.start({
node_id: vpnNodes[0].node_id,
subkind: 'wg',
})
// Start an OpenVPN session
const { uuid: uuid2 } = await client.services.vpn.start({
node_id: vpnNodes[0].node_id,
subkind: 'openvpn',
})
// Start a V2Ray session with vmess protocol
const { uuid: uuid3 } = await client.services.vpn.start({
node_id: vpnNodes[0].node_id,
subkind: 'v2ray',
protocol: 'vmess', // 'trojan' | 'vmess' | 'vless'
})After starting, get the VPN config from session info:
const info = await client.services.session(uuid).info()
console.log(info.config) // WireGuard / OpenVPN config string
// Note: Swagger spec names this field `vpn_config` but the real API returns `config`All service types (MR, Render, VPN) share the same session API.
const session = client.services.session('session-uuid')
// Poll until the session is ready
let info = await session.info()
while (!info.is_ready) {
await new Promise((r) => setTimeout(r, 2000))
info = await session.info()
}
// Session details
console.log('Public IP:', info.public_ip)
console.log('SSH direct:', info.ssh_direct ? `${info.ssh_direct.host}:${info.ssh_direct.port}` : '—')
console.log('SSH proxy:', info.ssh_proxy ? `${info.ssh_proxy.host}:${info.ssh_proxy.port}` : '—')
console.log('Web SSH:', info.ssh_web)
console.log('Web URLs:', info.urls) // { '8080': 'https://...' }
console.log('Port map:', info.ports_matrix) // { '22': 10022 }
console.log('Duration:', info.duration, 'seconds')
console.log('Charged:', info.charge_amount, 'Wei') // number or string-encoded integer for large values
// Get container and system logs
const logs = await session.logs()
console.log(logs.container)
for (const entry of logs.system) {
console.log(new Date(entry.ts).toISOString(), entry.msg)
}
// Stop the session (optionally rate it 1–5)
await session.stop({ score: 5 })// Active sessions
const active = await client.sessions.list()
// Recent (finished) sessions
const recent = await client.sessions.list({ recent: true })
for (const session of active) {
console.log(`${session.uuid} — ${session.service} — ${session.node_id}`)
}// Check status of a background idle job
const job = await client.idleJobs.get('node_id', 'job_id')
console.log('Running:', job.is_up)
console.log('Uptime:', job.uptime, 'seconds')
if (!job.is_up) {
console.error('Error:', job.error)
}
// Get job logs (SDK automatically decodes gzip+base64)
const logs = await client.idleJobs.logs('node_id', 'job_id')
console.log(logs)The SDK throws typed errors that are easy to catch specifically.
OctaError
├── OctaNetworkError — fetch failed (DNS, connection refused, etc.)
│ └── OctaTimeoutError — request timed out
└── OctaApiError — server returned an error response
├── OctaAuthenticationError — 401 Invalid API key
├── OctaPermissionError — 403 Forbidden
├── OctaNotFoundError — 404 Not Found
├── OctaValidationError — 422 Unprocessable Entity
├── OctaRateLimitError — 429 Too Many Requests
└── OctaServerError — 5xx Server Error
import {
OctaAuthenticationError,
OctaNotFoundError,
OctaRateLimitError,
OctaServerError,
OctaTimeoutError,
} from '@octaspace/sdk'
try {
const node = await client.nodes.get('unknown_node')
} catch (err) {
if (err instanceof OctaAuthenticationError) {
console.error('Invalid API key')
} else if (err instanceof OctaNotFoundError) {
console.error('Node not found')
} else if (err instanceof OctaRateLimitError) {
console.warn(`Rate limited — retry after ${err.retryAfter}s`)
} else if (err instanceof OctaServerError) {
console.error(`Server error ${err.status}: ${err.message}`)
} else if (err instanceof OctaTimeoutError) {
console.error('Request timed out')
} else {
throw err // re-throw unexpected errors
}
}All OctaApiError subclasses include:
| Property | Type | Description |
|---|---|---|
message |
string |
Human-readable error message |
status |
number |
HTTP status code |
code |
string | undefined |
Machine-readable error code from the API |
requestId |
string | undefined |
Request ID for support (from x-request-id header) |
body |
unknown |
Raw response body |
OctaRateLimitError additionally has:
| Property | Type | Description |
|---|---|---|
retryAfter |
number | undefined |
Seconds to wait before retrying |
const client = new OctaClient({
apiKey: process.env.OCTA_API_KEY,
timeoutMs: 10_000, // 10 seconds (default: 30 000)
})Use a short timeoutMs when constructing the client, or create a separate client instance for time-sensitive calls:
import { OctaTimeoutError } from '@octaspace/sdk'
const fastClient = new OctaClient({
apiKey: process.env.OCTA_API_KEY,
timeoutMs: 5_000,
})
try {
const machines = await fastClient.services.mr.available()
} catch (err) {
if (err instanceof OctaTimeoutError) {
console.error('Timed out after 5s')
}
}The SDK automatically retries failed GET requests on transient errors:
| Condition | Retried? |
|---|---|
| Network error | Yes |
| 429 Too Many Requests | Yes (respects Retry-After) |
| 5xx Server Error | Yes |
| 4xx Client Error | No |
| POST / PATCH requests | No |
Default: 2 retries with exponential backoff and full jitter.
// Globally
const client = new OctaClient({
apiKey: process.env.OCTA_API_KEY,
retries: 3, // default: 2
})
// Disable retries globally for this client
const noRetryClient = new OctaClient({ apiKey: process.env.OCTA_API_KEY, retries: 0 })Use hooks for logging, tracing, and monitoring without coupling to a specific library.
const client = new OctaClient({
apiKey: process.env.OCTA_API_KEY,
onRequest(ctx) {
console.log(`→ ${ctx.method} ${ctx.url} (attempt ${ctx.attempt + 1})`)
},
onResponse(ctx) {
console.log(`← ${ctx.status} in ${ctx.durationMs}ms`)
},
})import { trace } from '@opentelemetry/api'
const tracer = trace.getTracer('octa-sdk')
const spans = new WeakMap<object, ReturnType<typeof tracer.startSpan>>()
const client = new OctaClient({
apiKey: process.env.OCTA_API_KEY,
onRequest(ctx) {
const span = tracer.startSpan(`octa.${ctx.method} ${ctx.url}`)
spans.set(ctx, span)
},
onResponse(ctx) {
spans.get(ctx.request)?.setAttributes({ 'http.status_code': ctx.status }).end()
},
})Some endpoints return binary files. The SDK returns native Blob objects.
// Download ident file
const ident = await client.nodes.downloadIdent('node_id')
// → Blob
// Download node logs
const logs = await client.nodes.downloadLogs('node_id')
// → BlobSave to disk (Node.js):
import { writeFile } from 'node:fs/promises'
const blob = await client.nodes.downloadIdent('node_id')
await writeFile('node.ident', Buffer.from(await blob.arrayBuffer()))Trigger browser download:
const blob = await client.nodes.downloadLogs('node_id')
const url = URL.createObjectURL(blob)
Object.assign(document.createElement('a'), {
href: url,
download: 'node.log',
}).click()
URL.revokeObjectURL(url)Inject a custom fetch implementation for testing, proxying, or special environments.
import { describe, it, vi, expect } from 'vitest'
import { OctaClient } from '@octaspace/sdk'
it('returns account balance', async () => {
const mockFetch = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ balance: 1000 }), { status: 200 }),
)
const client = new OctaClient({ apiKey: 'test', fetch: mockFetch })
const { balance } = await client.accounts.balance()
expect(balance).toBe(1000)
})// app/api/octa/[...path]/route.ts
import { NextRequest } from 'next/server'
const OCTA_BASE = 'https://api.octa.computer'
export async function GET(req: NextRequest) {
const path = req.nextUrl.pathname.replace('/api/octa', '')
const upstream = await fetch(`${OCTA_BASE}${path}`, {
headers: { Authorization: process.env.OCTA_API_KEY! },
})
return upstream
}// Client-side — points to your proxy, no API key exposed
const client = new OctaClient({
apiKey: '', // not used through proxy
baseUrl: '/api/octa',
fetch: (url, init) => fetch(url, { ...init, headers: {} }),
})The SDK is written in TypeScript and ships with full type definitions. All types are exported and available for import.
import type {
OctaClientOptions,
MrMachine,
ServiceInfo,
VpnNode,
VpnSubkind,
NodePrices,
RequestContext,
ResponseContext,
} from '@octaspace/sdk'
// Type your own functions
function findCheapestGpu(machines: MrMachine[]): MrMachine | undefined {
return machines
.filter((m) => m.is_has_gpu)
.sort((a, b) => a.total_price_usd - b.total_price_usd)[0]
}
// Type-safe client options
const options: OctaClientOptions = {
apiKey: process.env.OCTA_API_KEY!,
timeoutMs: 15_000,
retries: 3,
onRequest: (ctx: RequestContext) => {
console.log(ctx.method, ctx.url)
},
}import type { VpnSubkind, VpnProtocol } from '@octaspace/sdk'
// VpnSubkind: 'wg' | 'openvpn' | 'ss' | 'v2ray'
// VpnProtocol: 'trojan' | 'vmess' | 'vless' (V2Ray only)The SDK works in modern browsers — it uses only the native fetch API.
<script type="module">
import { OctaClient } from 'https://esm.sh/@octaspace/sdk'
// ...
</script>Security: Never embed your API key in browser-side code — it will be visible to all users of your page. Use one of these patterns instead:
- Proxy all requests through your own server (recommended)
- Issue short-lived tokens scoped to specific operations
- Only use the public
network.get()endpoint client-side (no auth required)
| Environment | Minimum version |
|---|---|
| Node.js | 18.0.0 |
| Browser | Chrome 89, Firefox 90, Safari 15 |
| Deno | 1.28 |
| Bun | 1.0 |
| Cloudflare Workers | Supported |
Node.js 16 is supported by injecting a custom fetch:
import fetch from 'node-fetch'
const client = new OctaClient({
apiKey: process.env.OCTA_API_KEY,
fetch: fetch as typeof globalThis.fetch,
})const client = new OctaClient({
// Required
apiKey: string
// Optional
baseUrl?: string // default: 'https://api.octa.computer'
timeoutMs?: number // default: 30_000 (30 seconds)
retries?: number // default: 2
fetch?: typeof globalThis.fetch // custom fetch implementation
userAgent?: string // custom User-Agent string
// Hooks
onRequest?: (ctx: RequestContext) => void | Promise<void>
onResponse?: (ctx: ResponseContext) => void | Promise<void>
})MIT © OctaSpace