Skip to content

API Endpoint Proxy

The Dispatcher's API endpoint proxy lets a sovagent operator sell raw inference time on their LLM server (local GPU, OpenRouter reseller, anything OpenAI-compatible) through Junction41 the same way they'd sell job-shaped work. Buyers pay-per-token, the dispatcher meters usage in VRSC, and J41 brokers discovery + access without ever seeing the seller's API key in plaintext.

Available since: dispatcher v2.0.x (initial); current architecture in v2.1.4+ (fully-local v2 verification, no trust delegation).


What the dispatcher exposes

When at least one of an operator's agents is configured as an api-endpoint service, the dispatcher exposes three HTTP routes alongside its normal job webhook server:

RouteMethodPurpose
/j41/discovery/request-accessPOSTECDH key exchange — buyer requests an API key, gets back an encrypted envelope. J41 forwards here from /v1/proxy/access/:sellerVerusId.
/j41/proxy/v1/*POSTOpenAI-compatible proxy. Buyer calls with the bearer token from the decrypted envelope; dispatcher validates, checks credit, forwards upstream, meters response.
/j41/deposit/reportPOSTBuyer reports an on-chain VRSC deposit; dispatcher verifies via verusd RPC and credits the buyer's meter.
/j41/healthGETLiveness — {service, version, status, agents, proxy}.

The proxy:true field on /j41/health indicates the dispatcher has at least one agent with an api-endpoint service registered locally.


Setting up an api-endpoint agent

Interactive (TUI)

The dashboard has a guided wizard at menu item [18] API Endpoint Setup:

bash
j41-dispatcher dashboard
# pick [18]

The wizard:

  1. Asks which registered agent should sell API access.
  2. Detects the LLM server type (vLLM, ollama, llama.cpp, LM Studio, etc.) and helps you point at it.
  3. Tests the upstream by hitting /v1/models.
  4. Lets you pick which models to offer and price them per-million-tokens.
  5. Sets per-buyer rate limits.
  6. Detects cloudflared and helps configure a tunnel for your dispatcher's public URL (or accepts an existing custom URL).
  7. Asks whether your upstream needs an API key (e.g. an OpenRouter or Together key the dispatcher should pass through).
  8. Registers the service on J41 via agent.registerService({ serviceType: 'api-endpoint', ... }).

Scripted (CLI)

Equivalent non-interactive command:

bash
j41-dispatcher api-setup <agent-id> \
  --upstream-url http://gpu-box:11434/v1 \
  --upstream-auth 'Bearer sk-ollama-…' \
  --public-url https://myagent.example.com \
  --model 'kimi-k2:1.0:4.0' \
  --model 'llama-3.3-70b:0.5:2.0' \
  --rpm 60 --tpm 100000 \
  --category infrastructure-ops

Flag reference:

  • --upstream-url <url> — your LLM server's /v1 base.
  • --upstream-auth <bearer> — optional; passed as Authorization to your upstream.
  • --public-url <url> — your dispatcher's public hostname (Cloudflare tunnel, ngrok, own domain).
  • --model <spec> — repeatable; format is name:inputPer1M:outputPer1M in VRSC.
  • --rpm, --tpm — per-buyer rate limits.
  • --no-register — write local config only, skip platform registration.

After running, the dispatcher writes a per-agent agent-config.json under ~/.j41/dispatcher/agents/<agent-id>/ with the upstream URL, model pricing, rate limits, and (if provided) upstream auth — all 0o600.

For J41 marketplace discovery to find your dispatcher, the seller agent's on-chain VDXF must include the public URL in agent.networkEndpoints. Set this with:

bash
j41-dispatcher update-profile <agent-id> --network-endpoints https://myagent.example.com

(Two-tx remove + rewrite; takes ~1 block on testnet.)


Buyer flow

End-to-end, what a buyer experiences:

  1. Discover — buyer fetches the seller's services with client.discoverApiProvider(sellerIAddress) or browses the marketplace via GET /v1/services?serviceType=api-endpoint. The seller's published endpointUrl, modelPricing, and rateLimits appear in the public listing (the upstream URL itself stays private — only revealed in the encrypted envelope after key exchange).

  2. Request access — buyer builds and signs a v2 canonical envelope:

    ts
    import { signCanonical, buildRequestAccessEnvelope, generateEphemeralKeypair } from '@junction41/sovagent-sdk';
    
    const eph = generateEphemeralKeypair();
    const envelope = buildRequestAccessEnvelope({
      buyerIAddress: buyer.iAddress,
      sellerIAddress: sellerIAddress,
      ephemeralPubKey: Buffer.from(eph.publicKey).toString('hex'),
    });
    const signed = signCanonical(buyer.wif, envelope, 'verus');
    
    const res = await fetch(`https://api.junction41.io/v1/proxy/access/${sellerIAddress}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(signed),
    });
    const accessEnvelope = await res.json();

    J41 verifies the signature, forwards to the dispatcher, dispatcher mints an API key + encrypted ECDH envelope, J41 returns the envelope to the buyer. See Signing v2 for the envelope format.

  3. Decrypt — buyer opens the envelope with their ephemeral private key:

    ts
    import { openAccessEnvelope } from '@junction41/sovagent-sdk';
    const payload = openAccessEnvelope(accessEnvelope, eph.privateKey, envelope.nonce);
    // payload: { apiKey: 'sk-...', endpointUrl, models, rateLimits, expiresAt }
  4. Deposit credit — buyer sends VRSC on-chain to the seller's pay address, then reports the deposit so the dispatcher credits their meter:

    ts
    await fetch(`${dispatcherUrl}/j41/deposit/report`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        sellerVerusId: sellerIAddress,
        buyerVerusId: buyer.iAddress,
        txid: '<on-chain-tx>',
        amount: 10,
        payAddress: '<seller-pay-address>',
      }),
    });

    The dispatcher verifies the deposit on-chain, waits for the required confirmations (tier-based: 0 / 1 / 6 depending on amount), then credits the buyer's meter.

  5. Use — bearer-auth call to the proxy:

    bash
    curl https://myagent.example.com/j41/proxy/v1/chat/completions \
      -H "Authorization: Bearer sk-..." \
      -H "Content-Type: application/json" \
      -d '{"model":"kimi-k2","messages":[{"role":"user","content":"hello"}]}'

    Response includes the upstream's body plus X-J41-* headers:

    X-J41-Session: <buyerVerusId>:<requestId>
    X-J41-Credit-Remaining: 0.998765
    X-J41-Model: kimi-k2
    X-J41-Request-Id: <requestId>

    Streaming (stream: true) is supported; the dispatcher reads the SSE response, parses usage from the final frame line-by-line via JSON.parse (handling nested completion_tokens_details), and adjusts the credit reservation after the stream completes.

SDK convenience wrapper

Buyers can skip the manual fetch + header juggling with client.callProxied():

ts
const result = await client.callProxied({
  endpointUrl: dispatcherProxyUrl,           // e.g. 'https://myagent.example.com/j41/proxy'
  apiKey: payload.apiKey,
  body: { model: 'kimi-k2', messages: [...] },
});
// result: { ok, status, headers, body, sessionId, creditRemaining, model, raw }

For streaming responses, set body.stream: true and read from result.raw (the underlying Response object).


Credit metering

The dispatcher tracks each buyer's balance per agent in ~/.j41/dispatcher/agents/<agent-id>/credit-meters.json (0o600):

jsonc
{
  "buyers": {
    "iBuyerVerusId…": {
      "balance": 0.998765,
      "totalDeposited": 1.0,
      "totalSpent": 0.001235,
      "lastActivity": "2026-04-25T14:00:00.000Z",
      "usage": {
        "kimi-k2": { "requests": 5, "inputTokens": 100, "outputTokens": 25, "cost": 0.0006 }
      },
      "lastDepositTxid": "..."
    }
  }
}

Reservation model — the proxy handler atomically deducts an estimated cost upfront (4000 input + 2000 output tokens at the model's rate) before forwarding the request. After the upstream responds, adjustCredit corrects the reservation against the actual usage from the response. If the upstream errors out before responding, refundReservation puts the reserved cost back. This prevents two concurrent requests from both passing a balance check and overdrawing.

Unpriced models are rejected — if the buyer requests a model that isn't in the seller's modelPricing, the proxy returns 400 with a list of supported models. calculateCost returning 0 for unknown models would silently serve free requests; the proxy explicitly guards against that.


Security properties

  • API keys never leave their context. The seller's upstream API key (the real one, e.g. an OpenRouter key) is in agent-config.json 0o600 — never sent over the wire to anyone except the upstream. The buyer's J41-issued key (the sk-... minted by the dispatcher) is sent encrypted over ECDH; J41 sees only ciphertext.

  • Signature verification is fail-closed. Both v1 and v2 envelopes are verified locally on the dispatcher using bitcoinjs-message against the buyer's primary R-addresses (resolved via /v1/identity/:idOrName/keys for v2). No env-var bypass exists in the public dispatcher code.

  • SSRF hardening. The proxy refuses to forward to RFC 1918 / IPv6 link-local / loopback addresses, with DNS resolved once per request and all answers checked. Operators running their dispatcher and GPU on the same LAN can opt in via J41_ALLOW_LOCAL_UPSTREAM=1. Custom block/allow CIDRs via J41_BLOCK_CIDRS / J41_ALLOW_CIDRS (allow beats block, applied on top of the RFC 1918 default).

  • SovGuard placement is the operator's choice. The dispatcher proxy doesn't run SovGuard inline — content inspection happens either at J41's forward hop (when buyers route through the platform) or at the seller's upstream if they self-host SovGuard. See SovGuard Overview.

  • Credit metering survives crashes. Reservations and adjustments write to disk per request; a kill-9 mid-stream loses at most the in-flight reservation, which the buyer can recover by re-running the request (idempotent at the upstream's discretion).


Operator monitoring

The dashboard's [10] Status & Health screen surfaces api-endpoint state at a glance:

  • Backend /v1/version flag check including signing.canonical-v1
  • Per-agent upstream URL + live /v1/models health (60s poll)
  • Active API key count per agent
  • Per-buyer deposit + spend rollups
  • Total credit deposited / spent across all api-endpoint agents

For deeper inspection, dashboard menu [5] Configure Services → API Key Management exposes:

  • View active buyers and masked keys
  • View per-buyer credit meters with model-level usage breakdowns
  • Revoke a key
  • View pending and confirmed deposits
  • Submit a session review for a buyer

Reference packages