Account & Platform

Platform-level endpoints. These sit above both agent and project scope: they let you manage your own Juglans account, link external identities, register OAuth2 clients (so third parties can SSO with Juglans as the IdP), connect GitHub, and administer the agents you own.

Most endpoints below require a JWT bearer — the access token returned from POST /api/auth/login/google. JWT auth uses the same Authorization: Bearer ... header as API keys; the server distinguishes by token shape. The public discovery / market-data endpoints in the next section take no auth at all.

Base URLs

  • Production: https://api.juglans.ai
  • Local dev: http://localhost:3002

Public endpoints (no auth)

These respond unauthenticated — useful for discovery, OIDC handshakes, and read-only market data.

Method Path Purpose
GET /api/prices List spot prices for all tracked symbols.
GET /api/prices/{symbol} Spot price for one symbol (e.g. BTC, ETH).
GET /api/markets/perps List Hyperliquid perp markets.
GET /api/markets/perps/{symbol} One perp market by symbol.
GET /api/markets/predictions List Polymarket prediction markets.
GET /api/markets/predictions/{id} One prediction market by id.
GET /.well-known/openid-configuration OIDC discovery document.
GET /.well-known/jwks.json Public signing keys for verifying Juglans-issued JWTs.
GET /oauth2/authorize OAuth2 / OIDC authorization endpoint (browser entry point).
POST /oauth2/authorize Submit consent (browser-side form post).
POST /oauth2/token Exchange authorization code or refresh token for tokens.
POST /oauth2/revoke Revoke an issued access or refresh token.
GET /oauth2/userinfo OIDC userinfo — returns the subject's profile claims.

/oauth2/userinfo requires a Bearer access token issued by /oauth2/token, not a Juglans JWT — this is the standard OIDC contract for downstream relying parties.

Authentication

POST /api/auth/login/google

Exchange a Google ID token for a Juglans JWT. Creates the account on first call. Optional invite_code redeems an invite atomically (verified Google email must match the invite's bound email).

curl https://api.juglans.ai/api/auth/login/google \
  -H "Content-Type: application/json" \
  -d '{
    "id_token": "eyJhbGciOiJSUzI1NiIs...",
    "invite_code": "abc-def-ghi"
  }'

Request:

{
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "invite_code": "abc-def-ghi"
}

Response:

{
  "access_token": "eyJhbGciOi...",
  "refresh_token": "rt_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "account": {
    "id": "a1b2c3d4-1111-2222-3333-444455556666",
    "account_type": "human",
    "display_name": "Ada Lovelace",
    "email": "ada@example.com",
    "email_verified": true,
    "avatar_url": "https://lh3.googleusercontent.com/...",
    "status": "active",
    "metadata": {},
    "created_at": "2026-04-29T10:00:00Z",
    "updated_at": "2026-04-29T10:00:00Z"
  }
}

POST /api/auth/refresh

Trade a refresh token for a fresh access token.

Request:

{ "refresh_token": "rt_..." }

Response:

{
  "access_token": "eyJhbGciOi...",
  "refresh_token": "rt_...",
  "token_type": "Bearer",
  "expires_in": 3600
}

POST /api/auth/logout

Revoke the session backing the current access token.

curl -X POST https://api.juglans.ai/api/auth/logout \
  -H "Authorization: Bearer eyJhbGciOi..."

Response: { "ok": true }

Account profile

GET /api/account

Fetch the authenticated account.

curl https://api.juglans.ai/api/account \
  -H "Authorization: Bearer eyJhbGciOi..."

Response:

{
  "account": {
    "id": "a1b2c3d4-1111-2222-3333-444455556666",
    "account_type": "human",
    "display_name": "Ada Lovelace",
    "email": "ada@example.com",
    "email_verified": true,
    "avatar_url": "https://...",
    "status": "active",
    "metadata": {},
    "created_at": "2026-04-29T10:00:00Z",
    "updated_at": "2026-04-29T10:00:00Z"
  }
}

PATCH /api/account

Update the authenticated account's profile fields. All fields optional.

curl -X PATCH https://api.juglans.ai/api/account \
  -H "Authorization: Bearer eyJhbGciOi..." \
  -H "Content-Type: application/json" \
  -d '{"display_name": "Ada L.", "avatar_url": "https://example.com/me.png"}'

Request:

{
  "display_name": "Ada L.",
  "avatar_url": "https://example.com/me.png",
  "status": "active"
}

Response: same shape as GET /api/account.

Linked identities

External login providers attached to this account (Google, GitHub).

GET /api/account/identities

List identity links.

Response:

{
  "identities": [
    {
      "id": "11111111-2222-3333-4444-555566667777",
      "account_id": "a1b2c3d4-1111-2222-3333-444455556666",
      "provider": "google",
      "provider_sub": "104629876543210987654",
      "email": "ada@example.com",
      "metadata": { "picture": "https://..." },
      "created_at": "2026-04-29T10:00:00Z"
    }
  ]
}

POST /api/account/identities

Attach a new identity link manually. The GitHub OAuth flow (below) calls this internally.

Request:

{
  "provider": "github",
  "provider_sub": "9281467",
  "email": "ada@example.com"
}

Response:

{
  "identity": {
    "id": "11111111-2222-3333-4444-555566667777",
    "account_id": "a1b2c3d4-1111-2222-3333-444455556666",
    "provider": "github",
    "provider_sub": "9281467",
    "email": "ada@example.com",
    "metadata": {},
    "created_at": "2026-04-29T10:00:00Z"
  }
}

DELETE /api/account/identities/{id}

Unlink an identity. The id is the identity-link UUID, not the provider sub.

curl -X DELETE https://api.juglans.ai/api/account/identities/11111111-2222-3333-4444-555566667777 \
  -H "Authorization: Bearer eyJhbGciOi..."

Response: { "ok": true }

Wards & guardians

Juglans models each agent as its own first-class account. A guardianship is the row that says "human account A controls agent account B." Wards are the agents I guard; guardians are the humans guarding me.

GET /api/account/wards

List agent accounts I guard.

Response:

{
  "wards": [
    {
      "guardianship": {
        "id": "gg11-...",
        "guardian_account_id": "a1b2c3d4-...",
        "ward_account_id": "ww22-...",
        "role": "owner",
        "created_at": "2026-04-29T10:00:00Z",
        "updated_at": "2026-04-29T10:00:00Z"
      },
      "account": {
        "id": "ww22-...",
        "account_type": "agent",
        "display_name": "Nora",
        "email": null,
        "email_verified": false,
        "avatar_url": null,
        "status": "active",
        "metadata": {},
        "created_at": "2026-04-29T10:00:00Z",
        "updated_at": "2026-04-29T10:00:00Z"
      }
    }
  ]
}

POST /api/account/wards

Create a new agent account and establish guardianship over it.

Request:

{ "name": "Nora" }

name is optional — omit it and the server defaults to "New Agent".

Response:

{
  "account": { "id": "ww22-...", "account_type": "agent", "display_name": "Nora", "...": "..." },
  "guardianship": { "id": "gg11-...", "role": "owner", "...": "..." }
}

GET /api/account/guardians

List guardians of the authenticated account (i.e. who guards me — useful from an agent account's session).

Response:

{
  "guardians": [
    {
      "guardianship_id": "gg11-...",
      "role": "owner",
      "guardian": {
        "id": "a1b2c3d4-...",
        "display_name": "Ada Lovelace",
        "account_type": "human"
      }
    }
  ]
}

Sessions

GET /api/auth/sessions

List active sessions on this account (each POST /api/auth/login/google creates one).

Response:

{
  "sessions": [
    {
      "id": "ss11-2222-3333-4444-555566667777",
      "ip_address": "203.0.113.42",
      "user_agent": "Mozilla/5.0 ...",
      "created_at": "2026-04-29T10:00:00Z",
      "expires_at": "2026-05-29T10:00:00Z"
    }
  ]
}

DELETE /api/auth/sessions/{id}

Revoke a session by id. You can only revoke your own.

Response: { "ok": true }

OAuth2 client registration

Register an external app to use Juglans as an OIDC IdP. After registration, the client uses the standard OAuth2 endpoints (/oauth2/authorize, /oauth2/token, /oauth2/userinfo, plus /.well-known/openid-configuration for discovery) — not the /api/oauth2/clients ones, which are for managing clients.

POST /api/oauth2/clients

Register a new client. Returns the client_secret exactly once — store it.

Request:

{
  "client_name": "Acme Dashboard",
  "redirect_uris": ["https://acme.example.com/auth/callback"],
  "client_uri": "https://acme.example.com",
  "logo_uri": "https://acme.example.com/logo.png",
  "grant_types": ["authorization_code"],
  "scopes_allowed": ["openid", "profile", "email"],
  "is_confidential": true
}

Response:

{
  "client": {
    "id": "c1c2c3c4-1111-2222-3333-444455556666",
    "client_id": "jcl_a1b2c3d4e5f6...",
    "client_name": "Acme Dashboard",
    "redirect_uris": ["https://acme.example.com/auth/callback"],
    "grant_types": ["authorization_code"],
    "scopes_allowed": ["openid", "profile", "email"],
    "is_confidential": true,
    "created_at": "2026-04-29T10:00:00Z"
  },
  "client_secret": "jcs_e1f2a3b4..."
}

GET /api/oauth2/clients

List clients owned by the authenticated account.

Response:

{
  "clients": [
    {
      "id": "c1c2c3c4-...",
      "client_id": "jcl_a1b2c3d4...",
      "client_name": "Acme Dashboard",
      "redirect_uris": ["https://acme.example.com/auth/callback"],
      "status": "active",
      "created_at": "2026-04-29T10:00:00Z"
    }
  ]
}

GET /api/oauth2/clients/{id}

Fetch a single client (full row). 403 if you're not the owner.

Response: { "client": { ... } } — see the handler at crates/jg-server/src/handlers/oauth2.rs::get_client for the full shape.

PATCH /api/oauth2/clients/{id}

Update mutable fields. All fields optional.

Request:

{
  "client_name": "Acme Dashboard v2",
  "redirect_uris": ["https://acme.example.com/auth/callback", "https://acme.example.com/auth/callback2"],
  "client_uri": "https://acme.example.com",
  "logo_uri": "https://acme.example.com/logo.png",
  "status": "active"
}

Response: { "client": { ... } } — same shape as GET /api/oauth2/clients/{id}.

DELETE /api/oauth2/clients/{id}

Delete the client. Outstanding tokens issued to it remain valid until they expire.

Response: { "ok": true }

POST /api/oauth2/clients/{id}/rotate-secret

Generate a new client_secret and invalidate the old one. Returned exactly once.

Response:

{ "client_secret": "jcs_a1b2c3d4..." }

GitHub integration

Two flows live under /api/github/*: the App install flow (so Juglans can write commits to a user's repo on their behalf) and the OAuth user-authorization flow (so we learn the user's GitHub login + numeric id and store it as an identity link).

GET /api/github/install-url

Return the URL the SPA should open in a new tab to install the Juglans GitHub App. The redirect target is configured in GitHub App settings.

Response:

{ "url": "https://github.com/apps/juglans/installations/new" }

GET /api/github/callback?installation_id=...&setup_action=install

Called by the SPA after the user completes the GitHub-side install. Resolves the installation and persists a github_installations row on the calling account.

Response:

{
  "installation": {
    "id": 12345678,
    "account_login": "ada-lovelace",
    "target_type": "User"
  }
}

GET /api/github/oauth/url

Return the GitHub user-authorization URL. Distinct from the install URL — this learns the user's GitHub identity (login + numeric id) so we can auto-fill collaborator invites.

Response:

{ "url": "https://github.com/login/oauth/authorize?client_id=Iv1.abc123..." }

POST /api/github/oauth/exchange

Exchange the OAuth code from the user-authorization redirect for a GitHub access token, fetch the GitHub profile, and persist as an account_identity_links row with provider=github.

Request:

{ "code": "abcdef0123456789" }

Response:

{
  "identity_link": {
    "id": "11111111-2222-3333-4444-555566667777",
    "provider": "github",
    "login": "ada-lovelace",
    "email": "ada@example.com"
  }
}

GET /api/github/installations

List GitHub App installations linked to the authenticated account. Pass ?with_repos=true to hydrate each item with the repos it grants — note this is n+1 calls to GitHub.

Response (no with_repos):

{
  "items": [
    {
      "installation_id": 12345678,
      "account_login": "ada-lovelace",
      "target_type": "User"
    }
  ]
}

Response (with_repos=true): same shape, plus a repos field on each item. See the handler at crates/jg-server/src/handlers/github.rs::list_installations for the full per-repo shape.

POST /api/github/installations/claim

Manual recovery for failed callbacks. The user looks up the installation id at https://github.com/settings/installations and pastes it here.

Request:

{ "installation_id": 12345678 }

Response: same as GET /api/github/callback.

DELETE /api/github/installations/{installation_id}

Unbind a previously-linked installation from this Juglans account. The GitHub-side install is untouched — to fully revoke the App, the user goes to GitHub's settings page.

Response: 204 No Content

Agent administration

Owner-side endpoints for the agents you guard. Every path validates that the caller's account is in account_guardianships for the target agent — a 404 (not 403) is returned for foreign agents to avoid leaking existence.

POST /api/agents

Create a new agent under the authenticated account. Returns the freshly-minted jg_a_* API key once — store it server-side immediately. The handler also seeds the agent's filesystem dir with default templates and (best-effort) creates a backing GitHub repo.

curl -X POST https://api.juglans.ai/api/agents \
  -H "Authorization: Bearer eyJhbGciOi..." \
  -H "Content-Type: application/json" \
  -d '{"name": "Nora", "avatar_url": "https://example.com/nora.png"}'

Request:

{
  "name": "Nora",
  "avatar_url": "https://example.com/nora.png"
}

Response:

{
  "agent": {
    "id": "ag11-2222-3333-4444-555566667777",
    "owner_account_id": "a1b2c3d4-1111-2222-3333-444455556666",
    "name": "Nora",
    "status": "active",
    "test_mode": false,
    "wallet_config": {},
    "avatar_url": "https://example.com/nora.png",
    "git_remote_url": "git@github.com:juglans-ai/agent-ag11-2222-3333-4444-555566667777.git",
    "git_remote_mode": "juglans",
    "created_at": "2026-04-29T10:00:00Z",
    "updated_at": "2026-04-29T10:00:00Z"
  },
  "api_key": "jg_a_a1b2c3d4e5f6...",
  "api_key_id": "kk11-2222-3333-4444-555566667777"
}

GET /api/agents

List agents owned by the authenticated account. Optional ?mode=live|test filters by test_mode.

Response:

{
  "agents": [
    {
      "id": "ag11-2222-3333-4444-555566667777",
      "name": "Nora",
      "status": "active",
      "test_mode": false,
      "avatar_url": "https://example.com/nora.png",
      "created_at": "2026-04-29T10:00:00Z",
      "updated_at": "2026-04-29T10:00:00Z"
    }
  ]
}

GET /api/agents/{id}

Fetch one agent with its wallets and connected exchanges.

Response:

{
  "agent": { "id": "ag11-...", "name": "Nora", "status": "active", "...": "..." },
  "wallets": [
    { "id": "iw_clx...", "chain": "ethereum", "address": "0xAb12...CdEf" }
  ],
  "exchanges": [
    { "exchange": "hyperliquid", "permissions": ["read","trade"], "connected_at": "2026-04-29T10:00:00Z" }
  ]
}

PATCH /api/agents/{id}

Update mutable agent fields. All optional. Pass status: "frozen" to disable trading without deleting the agent.

Request:

{
  "name": "Nora v2",
  "status": "active",
  "avatar_url": "https://example.com/nora2.png",
  "wallet_config": { "default_chain": "ethereum" }
}

Response: { "agent": { ... } } — same shape as GET /api/agents/{id}'s agent field.

POST /api/agents/{id}/keys

Mint a new jg_a_* API key for the agent. The plaintext key is returned exactly once; only the hash is persisted. Always issued with the full scope set: read, trade, transfer, admin.

curl -X POST https://api.juglans.ai/api/agents/ag11-2222-3333-4444-555566667777/keys \
  -H "Authorization: Bearer eyJhbGciOi..."

Response:

{
  "api_key": "jg_a_b2c3d4e5f6a7...",
  "api_key_id": "kk22-3333-4444-5555-666677778888",
  "prefix": "jg_a_b2c3d4e"
}

POST /api/agents/{id}/avatar

Upload an avatar via multipart/form-data with a single file field. Cap: 3 MiB request body, 2 MiB image. Allowed content types: image/png, image/jpeg, image/webp, image/gif. The server stores under /uploads/agents/{id}.{ext} and updates avatar_url with a cache-busted query string.

curl -X POST https://api.juglans.ai/api/agents/ag11-2222-3333-4444-555566667777/avatar \
  -H "Authorization: Bearer eyJhbGciOi..." \
  -F "file=@./nora.png;type=image/png"

Response: { "agent": { ... } } with the updated avatar_url.

POST /api/agents/{id}/wallets

Provision a new server-custodied wallet via Privy and bind it to the agent.

Request:

{ "chain_type": "ethereum" }

Response:

{
  "id": "ww11-2222-3333-4444-555566667777",
  "chain": "ethereum",
  "address": "0xAb12...CdEf",
  "privy_wallet_id": "iw_clxabc123..."
}

POST /api/agents/{id}/card

Issue a virtual card for the agent (Lithic-backed). Card data including PAN/CVV is returned on the create response — capture it now.

Response:

{
  "card": {
    "token": "card_01H8X7...",
    "last_four": "4242",
    "type": "VIRTUAL",
    "exp_month": "04",
    "exp_year": "2030",
    "spend_limit": 10000,
    "state": "OPEN",
    "pan": "4111111111114242",
    "cvv": "123"
  }
}

GET /api/agents/{id}/card

List cards issued to this agent. PAN/CVV are not returned here — only the safe metadata.

Response:

{
  "cards": [
    {
      "token": "card_01H8X7...",
      "last_four": "4242",
      "type": "VIRTUAL",
      "exp_month": "04",
      "exp_year": "2030",
      "spend_limit": 10000,
      "state": "OPEN"
    }
  ]
}

PUT /api/agents/{id}/limits

Set or update a spending limit. Periods are minute, hour, or day. asset defaults to USDC.

Request:

{ "period": "day", "max_amount": "500.00", "asset": "USDC" }

Response:

{
  "limits": [
    { "period": "day", "max_amount": "500.00", "asset": "USDC" }
  ]
}

GET /api/agents/{id}/limits

List configured limits for the agent.

Response: same shape as PUT /api/agents/{id}/limits.

GET /api/agents/{id}/audit

Recent audit-log entries (every API-key request that hit the agent). Optional ?limit=N (default 50, max 200).

Response:

{
  "audit_log": [
    {
      "id": "ll11-2222-3333-4444-555566667777",
      "action": "place_order",
      "details": { "symbol": "BTC", "side": "buy", "size": "0.1" },
      "spending_limit_remaining": "499.40",
      "created_at": "2026-04-29T10:05:12Z"
    }
  ]
}

POST /api/agents/{id}/exchanges

Connect the agent to a centralized exchange. Sends on-chain approvals via Privy (USDC for polymarket on Polygon, USDC for hyperliquid on Arbitrum) when the agent is in live mode.

Request:

{ "exchange": "hyperliquid" }

Response:

{
  "exchange": "hyperliquid",
  "status": "active",
  "wallet": "0xAb12...CdEf",
  "tx_hashes": ["0x9f8e7d..."]
}

DELETE /api/agents/{id}/exchanges/{ex_id}

Disconnect the named exchange. {ex_id} is the exchange short name (e.g. hyperliquid), not a UUID.

Response: { "status": "disconnected", "exchange": "hyperliquid" }

GET /api/agents/{id}/memories

List the agent's filesystem-backed memory entries (owner-side view).

Response:

{
  "memories": [
    {
      "key": "trading_style",
      "content": "Conservative — never more than 5% of NAV in a single position.",
      "metadata": {},
      "updated_at": "2026-04-29T10:00:00Z",
      "updated_by": "user"
    }
  ]
}

GET /api/agents/{id}/memories/{key}

Fetch one memory entry by key.

Response: a single entry object with the same fields as items in memories[] above.

PUT /api/agents/{id}/memories/{key}

Owner-side write. Owner writes always set updated_by = "user", which the agent-side write path respects (the agent must pass force=true to overwrite a user-edited memory).

Request:

{
  "content": "Conservative — never more than 5% of NAV in a single position.",
  "metadata": { "source": "owner" }
}

Response: the written entry.

DELETE /api/agents/{id}/memories/{key}

Delete a memory entry. Response: { "deleted": true }.

GET /api/agents/{id}/skill.md

Download a personalized Claude Code skill bundle for this agent. Response is text/markdown; charset=utf-8 with Content-Disposition: attachment; filename="juglans-{slug}.skill.md".

GET /api/agents/{id}/openapi.yaml

Personalized OpenAPI 3 spec for ChatGPT Actions / Manus / any OpenAPI-aware host. Response is application/yaml.

GET /api/agents/{id}/gpt-instructions.txt

Custom GPT Instructions text. Response is text/plain.

GET /api/agents/{id}/cursorrules

.cursorrules text — drop into a repo root for Cursor. Response is text/plain.

GET /api/agents/{id}/chat-workflow

Read the agent's chat workflow files: chat.jg, juglans.toml, and prompts/system_prompt.jgx. Each field is paired with an is_default_* boolean indicating whether the on-disk content matches the bundled default.

Response:

{
  "chat_jg": "flow chat { ... }",
  "is_default_chat_jg": true,
  "juglans_toml": "[ai.providers.juglans]\nbase_url = \"https://api.juglans.ai/api/llm\"\n...",
  "is_default_juglans_toml": true,
  "system_prompt_jgx": "You are Nora, an autonomous trading agent...",
  "is_default_system_prompt_jgx": false
}

PUT /api/agents/{id}/chat-workflow

Update any subset of the three workflow files. Writes commit + push to the agent's git remote when bound.

Request:

{
  "chat_jg": "flow chat { ... }",
  "juglans_toml": "[ai.providers.juglans]\n...",
  "system_prompt_jgx": "You are Nora..."
}

Response: { "updated": true }

DELETE /api/agents/{id}/chat-workflow?field=…

Reset one or all workflow files to their bundled defaults. field must be chat_jg, juglans_toml, system_prompt_jgx, or all.

Response: { "reset": true }

PATCH /api/agents/{id}/git

Rebind the agent's repository to a user-owned GitHub repo reached through a previously-installed App installation. Force-pushes the agent's working copy into the new repo and installs a webhook on it.

Request:

{
  "installation_id": 12345678,
  "repo": "ada-lovelace/nora-agent"
}

The body also accepts split fields (owner + name) in lieu of repo. Response:

{
  "agent": { "id": "ag11-...", "git_remote_url": "git@github.com:ada-lovelace/nora-agent.git", "git_remote_mode": "byo", "git_installation_id": 12345678, "...": "..." }
}

POST /api/agents/{id}/repo/grant-access

Invite a GitHub user to the agent's hosted repo as a collaborator. Solves the visibility problem in Juglans-hosted mode where the repo is private under the platform org.

Request:

{ "github_username": "ada-lovelace", "permission": "push" }

permission defaults to push; valid values follow GitHub's collaborator permission model (pull, triage, push, maintain, admin). Response:

{
  "ok": true,
  "owner": "juglans-ai",
  "repo": "agent-ag11-2222-3333-4444-555566667777",
  "github_username": "ada-lovelace",
  "permission": "push"
}

GET /api/skills

List the server's bundled starter-skill catalog. Identical for every authenticated user; returns the parsed YAML frontmatter (slug, description) and the full .jgx body so the frontend editor can render them as read-only tabs.

Response:

{
  "skills": [
    {
      "filename": "place-order.jgx",
      "slug": "place-order",
      "description": "Place a perp order on Hyperliquid.",
      "content": "---\nslug: place-order\ndescription: ...\n---\n...",
      "is_bundled": true
    }
  ]
}

Owner-side conversations

The web UI stores chat history server-side so threads persist across reloads and can be enumerated in a sidebar. These endpoints are JWT-only — they're the owner's conversations, distinct from the per-project conversations the company exposes via jg_p_* keys (see the projects doc).

POST /api/chat

Server-Sent-Events streaming chat with one of the caller's agents. Auto-creates a conversation on first call (returned in the first SSE event as {type: "meta", conversation_id: "..."}); pass conversation_id back on subsequent calls to keep the same thread.

curl -N https://api.juglans.ai/api/chat \
  -H "Authorization: Bearer eyJhbGciOi..." \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "ag11-2222-3333-4444-555566667777",
    "message": "What positions do we hold on Hyperliquid right now?",
    "conversation_id": "cv11-2222-3333-4444-555566667777"
  }'

Request:

{
  "agent_id": "ag11-2222-3333-4444-555566667777",
  "message": "What positions do we hold on Hyperliquid right now?",
  "conversation_id": "cv11-2222-3333-4444-555566667777"
}

conversation_id is optional. Response is text/event-stream with these event payload shapes:

data: {"type":"meta","conversation_id":"cv11-2222-3333-4444-555566667777"}
data: {"type":"content","text":"You currently hold..."}
data: {"type":"content","text":" 0.5 BTC perp at $..."}
data: {"type":"done"}

Errors arrive as {type: "error", message: "..."}. The server persists the user message before invoking juglans and the assistant reply once the stream completes — a client disconnect mid-stream does not lose either.

GET /api/conversations

List the caller's conversations with derived participant lists.

Response:

{
  "conversations": [
    {
      "id": "cv11-2222-3333-4444-555566667777",
      "account_id": "a1b2c3d4-1111-2222-3333-444455556666",
      "project_id": null,
      "external_user_id": null,
      "title": "Daily perp review",
      "created_at": "2026-04-29T09:00:00Z",
      "last_message_at": "2026-04-29T10:05:12Z",
      "archived_at": null,
      "agent_ids": ["ag11-2222-3333-4444-555566667777"]
    }
  ]
}

POST /api/conversations

Create an empty conversation. Useful when you want to set the title before the first message.

Request:

{ "title": "Daily perp review" }

Response: { "conversation": { ... } } — the freshly-created row with agent_ids: [].

GET /api/conversations/{id}

Fetch one conversation along with its full message log. 404 if the row's account_id doesn't match the caller (existence is hidden across owners).

Response:

{
  "conversation": { "id": "cv11-...", "title": "Daily perp review", "...": "..." },
  "messages": [
    {
      "id": "mg11-2222-3333-4444-555566667777",
      "role": "user",
      "agent_id": null,
      "content": "What positions do we hold on Hyperliquid right now?",
      "created_at": "2026-04-29T10:05:00Z"
    },
    {
      "id": "mg22-3333-4444-5555-666677778888",
      "role": "assistant",
      "agent_id": "ag11-2222-3333-4444-555566667777",
      "content": "You currently hold 0.5 BTC perp...",
      "created_at": "2026-04-29T10:05:12Z"
    }
  ]
}

PATCH /api/conversations/{id}

Rename or (un)archive a conversation. Both fields optional; archived: true archives, false restores, null/omitted leaves alone.

Request:

{ "title": "Daily perp review (Q2)", "archived": false }

Response: { "conversation": { ... } } with the updated metadata.

DELETE /api/conversations/{id}

Permanently delete the conversation and its messages.

Response: 204 No Content

Wallet inspection

Read-only proxies to Privy for the wallets attached to your agents. Wallet creation lives under the agent-administration section above (POST /api/agents/{id}/wallets); these routes only inspect.

GET /api/wallets/{wallet_id}

Fetch wallet details from Privy. {wallet_id} is the Privy id (e.g. iw_clxabc123...), not the database row id.

curl https://api.juglans.ai/api/wallets/iw_clxabc123... \
  -H "Authorization: Bearer eyJhbGciOi..."

Response: the raw Privy wallet object — see the handler at crates/jg-server/src/handlers/wallet.rs::get_wallet for the full shape.

GET /api/wallets/{wallet_id}/balance

Fetch a balance. Optional query params: chain (default ethereum) and asset (default eth).

curl "https://api.juglans.ai/api/wallets/iw_clxabc123.../balance?chain=eip155:1&asset=native" \
  -H "Authorization: Bearer eyJhbGciOi..."

Response:

{
  "balances": [
    { "chain": "eip155:1", "asset": "native", "amount": "0.4210", "decimals": 18 }
  ]
}

Endpoint index

Full table of routes covered by this document. Auth column: None = no token, JWT = Juglans access token from /api/auth/login/google, OIDC = Bearer access token issued by /oauth2/token (relying-party use), HMAC = GitHub-signed webhook payload.

Method Path Auth
GET /api/prices None
GET /api/prices/{symbol} None
GET /api/markets/perps None
GET /api/markets/perps/{symbol} None
GET /api/markets/predictions None
GET /api/markets/predictions/{id} None
GET /.well-known/openid-configuration None
GET /.well-known/jwks.json None
GET /oauth2/authorize None
POST /oauth2/authorize None
POST /oauth2/token None
POST /oauth2/revoke None
GET /oauth2/userinfo OIDC
POST /api/auth/login/google None
POST /api/auth/refresh None
POST /api/auth/logout JWT
GET /api/auth/sessions JWT
DELETE /api/auth/sessions/{id} JWT
GET /api/account JWT
PATCH /api/account JWT
GET /api/account/identities JWT
POST /api/account/identities JWT
DELETE /api/account/identities/{id} JWT
GET /api/account/wards JWT
POST /api/account/wards JWT
GET /api/account/guardians JWT
POST /api/oauth2/clients JWT
GET /api/oauth2/clients JWT
GET /api/oauth2/clients/{id} JWT
PATCH /api/oauth2/clients/{id} JWT
DELETE /api/oauth2/clients/{id} JWT
POST /api/oauth2/clients/{id}/rotate-secret JWT
GET /api/github/install-url JWT
GET /api/github/callback JWT
GET /api/github/oauth/url JWT
POST /api/github/oauth/exchange JWT
GET /api/github/installations JWT
POST /api/github/installations/claim JWT
DELETE /api/github/installations/{installation_id} JWT
POST /api/agents JWT
GET /api/agents JWT
GET /api/agents/{id} JWT
PATCH /api/agents/{id} JWT
POST /api/agents/{id}/keys JWT
POST /api/agents/{id}/avatar JWT
POST /api/agents/{id}/wallets JWT
POST /api/agents/{id}/card JWT
GET /api/agents/{id}/card JWT
PUT /api/agents/{id}/limits JWT
GET /api/agents/{id}/limits JWT
GET /api/agents/{id}/audit JWT
POST /api/agents/{id}/exchanges JWT
DELETE /api/agents/{id}/exchanges/{ex_id} JWT
GET /api/agents/{id}/memories JWT
GET /api/agents/{id}/memories/{key} JWT
PUT /api/agents/{id}/memories/{key} JWT
DELETE /api/agents/{id}/memories/{key} JWT
GET /api/agents/{id}/skill.md JWT
GET /api/agents/{id}/openapi.yaml JWT
GET /api/agents/{id}/gpt-instructions.txt JWT
GET /api/agents/{id}/cursorrules JWT
GET /api/agents/{id}/chat-workflow JWT
PUT /api/agents/{id}/chat-workflow JWT
DELETE /api/agents/{id}/chat-workflow JWT
PATCH /api/agents/{id}/git JWT
POST /api/agents/{id}/repo/grant-access JWT
GET /api/skills JWT
POST /api/chat JWT
GET /api/conversations JWT
POST /api/conversations JWT
GET /api/conversations/{id} JWT
PATCH /api/conversations/{id} JWT
DELETE /api/conversations/{id} JWT
GET /api/wallets/{wallet_id} JWT
GET /api/wallets/{wallet_id}/balance JWT