npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@octaspace/sdk

v0.1.1

Published

Official JavaScript/TypeScript SDK for the OCTA API

Readme

@octaspace/sdk

Official JavaScript/TypeScript SDK for the OCTA API.

npm version npm downloads License: MIT Node.js


Table of Contents


Installation

npm install @octaspace/sdk
# or
pnpm add @octaspace/sdk
# or
yarn add @octaspace/sdk

Quick Start

import { 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 })

Authentication

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.


Resources

Accounts

// 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()

Network

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}`)

Apps

// 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')

Nodes

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)

Services — Machine Rental

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',
})

Services — Render

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' },
})

Services — VPN

// 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'
})

// Start an HTTP proxy session
const { uuid: uuid4 } = await client.services.vpn.start({
  node_id: vpnNodes[0].node_id,
  subkind: 'http_proxy',
})

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`

// For http_proxy sessions, `config` is an object instead of a string:
if (typeof info.config === 'object') {
  const { ip, port } = info.config  // ProxyConfig
}

Session Lifecycle

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 })

Sessions List

// 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}`)
}

Idle Jobs

// 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)

Error Handling

The SDK throws typed errors that are easy to catch specifically.

Error Hierarchy

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

Usage

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
  }
}

Error Properties

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 |


Timeouts & Cancellation

Global timeout

const client = new OctaClient({
  apiKey: process.env.OCTA_API_KEY,
  timeoutMs: 10_000,  // 10 seconds (default: 30 000)
})

Per-request timeout

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')
  }
}

Retries

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.

Configuration

// 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 })

Request & Response Hooks

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`)
  },
})

OpenTelemetry example

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()
  },
})

Binary Responses

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')
// → Blob

Save 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)

Custom Fetch / Proxy

Inject a custom fetch implementation for testing, proxying, or special environments.

Testing with mocks

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)
})

Server-side proxy (Next.js Route Handler)

// 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: {} }),
})

TypeScript

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)
  },
}

VPN subkind and protocol types

import type { VpnSubkind, VpnProtocol } from '@octaspace/sdk'

// VpnSubkind: 'wg' | 'openvpn' | 'ss' | 'v2ray' | 'http_proxy'
// VpnProtocol: 'trojan' | 'vmess' | 'vless'  (V2Ray only)

Browser Usage

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)

Requirements

| 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,
})

All Constructor Options

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>
})

License

MIT © OctaSpace