RAM Triage — API

Drive the memory triage from a script, in eight languages.

API tokens Open the app

Triage a capture over HTTP

RAM Triage is a SkillSafe app, so everything the page does is available as a REST API: post the text output of your Volatility 3 plugin runs and get back one structured JSON report — suspicious processes each with a verbatim evidence quote, injected-memory findings, network indicators, persistence, an IOC table, a timeline, containment steps and the follow-up commands to run next. This page documents the exact request and response shapes the app itself uses; nothing here is a simplification of them.

Base URL and the response envelope

Every endpoint below is relative to this base, and every call carries a bearer token:

BASE https://api.skillsafe.ai/v1/app-api

Success and failure share one envelope. A success body is {"ok":true,"data":{...}}; a failure is {"ok":false,"error":{"code":"...","message":"...","details":{...}}}. Read data on success and error.code on failure — the HTTP status mirrors the code but the body is the authoritative description.

Endpoints used on this page

POST /guest GET /me POST /estimate POST /run GET /jobs/{job_id} POST /run-stream POST /collections/cases/query

Error codes

CodeHTTPWhat it means and what to do
unauthorized401Missing, malformed or expired token. Mint a new one (step 1). Guest tokens expire; personal tokens do not, but can be rotated.
forbidden403The token is valid but not for this app. Tokens are scoped per app slug.
payment_required402Balance is below min_credits. Call /estimate first and compare against /me so you never reach this.
validation_error400The input object is the wrong shape. error.details names the field. dump_output must be a non-empty string.
rate_limited429Back off and retry. Vector search on the cases collection is limited to 30 requests per minute per IP, tighter than the other data endpoints.
not_found404Unknown job_id, record id, or collection name.
sponsor_exhausted402Returned in error.details.reason when a guest run exceeds the publisher's sponsored allowance. Sign in and run on your own credits.
internal_error500Retry once with the same Idempotency-Key; the platform will not double-bill a repeat of a key it has already settled.

1. Get a token

The simplest path is the point-and-click one: open the token page, sign in if you want the run billed to your own account, and use Copy shell export. It hands you export SKILLSAFE_TOKEN="aut_..." ready to paste. You never need to open a browser console to find a token.

For a fully scripted flow, mint a guest token instead. A guest can call /me and /estimate freely; /run works only if this app sponsors guest usage, which it does not by default, so a triage run needs a personal token.

POST /guest
export API="https://api.skillsafe.ai/v1/app-api"
export TOKEN="YOUR_TOKEN"     # from /tokens.html, or the guest call below

curl -s -X POST "$API/guest" \
  -H "Content-Type: application/json" \
  -d '{"slug":"ram-triage"}' | jq -r '.data.token'
import json, urllib.request

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"          # from /tokens.html

req = urllib.request.Request(
    API + "/guest",
    data=json.dumps({"slug": "ram-triage"}).encode(),
    headers={"Content-Type": "application/json"},
    method="POST",
)
with urllib.request.urlopen(req) as r:
    TOKEN = json.load(r)["data"]["token"]
print(TOKEN)
const API = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN";     // from /tokens.html

const res = await fetch(API + "/guest", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ slug: "ram-triage" }),
});
TOKEN = (await res.json()).data.token;
console.log(TOKEN);
package main

import (
  "bytes"
  "encoding/json"
  "fmt"
  "net/http"
  "os"
)

const API = "https://api.skillsafe.ai/v1/app-api"

func main() {
  token := os.Getenv("SKILLSAFE_TOKEN") // or the guest call below
  body, _ := json.Marshal(map[string]string{"slug": "ram-triage"})
  res, err := http.Post(API+"/guest", "application/json", bytes.NewReader(body))
  if err != nil {
    panic(err)
  }
  defer res.Body.Close()
  var env struct {
    Data struct{ Token string `json:"token"` } `json:"data"`
  }
  json.NewDecoder(res.Body).Decode(&env)
  if env.Data.Token != "" {
    token = env.Data.Token
  }
  fmt.Println(token)
}
import java.net.URI;
import java.net.http.*;

public class Step1 {
  static final String API = "https://api.skillsafe.ai/v1/app-api";

  public static void main(String[] args) throws Exception {
    String token = System.getenv("SKILLSAFE_TOKEN"); // or the guest call below
    HttpClient http = HttpClient.newHttpClient();
    HttpRequest req = HttpRequest.newBuilder(URI.create(API + "/guest"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"ram-triage\"}"))
        .build();
    String body = http.send(req, HttpResponse.BodyHandlers.ofString()).body();
    System.out.println(body); // {"ok":true,"data":{"token":"aut_..."}}
  }
}
require "json"
require "net/http"

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV["SKILLSAFE_TOKEN"] || "YOUR_TOKEN"

uri = URI("#{API}/guest")
res = Net::HTTP.post(uri, { slug: "ram-triage" }.to_json,
                     "Content-Type" => "application/json")
puts JSON.parse(res.body)["data"]["token"]
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$token = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";

$ch = curl_init(API . "/guest");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
  CURLOPT_POSTFIELDS => json_encode(["slug" => "ram-triage"]),
  CURLOPT_RETURNTRANSFER => true,
]);
$env = json_decode(curl_exec($ch), true);
echo $env["data"]["token"], "\n";
using System.Net.Http.Json;
using System.Text.Json;

const string API = "https://api.skillsafe.ai/v1/app-api";
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";

using var http = new HttpClient();
var res = await http.PostAsJsonAsync(API + "/guest", new { slug = "ram-triage" });
var env = await res.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(env.GetProperty("data").GetProperty("token").GetString());
Treat the token like a password: it can spend credits through this app. The token page can also forget the local copy, which is not the same as server-side revocation — guest tokens simply expire.

2. A tiny client helper

Every later step assumes this helper: it prefixes the base URL, attaches the bearer token, and unwraps the data envelope so calling code deals in plain objects. It also raises on ok:false instead of silently returning a failure body.

# cURL has no helper - the pattern below is used verbatim in every step:
#   curl -s "$API/PATH" -H "Authorization: Bearer $TOKEN" [-d '{json}']
# and jq pulls fields out of the {"data": ...} envelope:
#   ... | jq '.data'
#   ... | jq -r '.error.code // empty'   # non-empty means the call failed
import json, urllib.error, urllib.request

def api(method, path, body=None, extra_headers=None):
    headers = {"Authorization": "Bearer " + TOKEN}
    data = None
    if body is not None:
        headers["Content-Type"] = "application/json"
        data = json.dumps(body).encode()
    headers.update(extra_headers or {})
    req = urllib.request.Request(API + path, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req) as r:
            return json.load(r)["data"]
    except urllib.error.HTTPError as e:
        env = json.load(e)
        err = env.get("error", {})
        raise RuntimeError("%s: %s" % (err.get("code"), err.get("message")))
async function api(method, path, body, extraHeaders) {
  const headers = { Authorization: "Bearer " + TOKEN, ...(extraHeaders || {}) };
  if (body !== undefined) headers["Content-Type"] = "application/json";
  const res = await fetch(API + path, {
    method,
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const env = await res.json();
  if (!env.ok) throw new Error(env.error.code + ": " + env.error.message);
  return env.data;
}
func api(method, path string, body any, extra map[string]string, out any) error {
  var rdr io.Reader
  if body != nil {
    b, _ := json.Marshal(body)
    rdr = bytes.NewReader(b)
  }
  req, _ := http.NewRequest(method, API+path, rdr)
  req.Header.Set("Authorization", "Bearer "+token)
  if body != nil {
    req.Header.Set("Content-Type", "application/json")
  }
  for k, v := range extra {
    req.Header.Set(k, v)
  }
  res, err := http.DefaultClient.Do(req)
  if err != nil {
    return err
  }
  defer res.Body.Close()
  var env struct {
    OK    bool            `json:"ok"`
    Data  json.RawMessage `json:"data"`
    Error struct {
      Code    string `json:"code"`
      Message string `json:"message"`
    } `json:"error"`
  }
  if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
    return err
  }
  if !env.OK {
    return fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
  }
  return json.Unmarshal(env.Data, out)
}
static String api(String method, String path, String jsonBody, Map<String, String> extra)
    throws Exception {
  HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(API + path))
      .header("Authorization", "Bearer " + token);
  if (jsonBody == null) {
    b.method(method, HttpRequest.BodyPublishers.noBody());
  } else {
    b.header("Content-Type", "application/json")
     .method(method, HttpRequest.BodyPublishers.ofString(jsonBody));
  }
  if (extra != null) extra.forEach(b::header);
  HttpResponse<String> res = HttpClient.newHttpClient()
      .send(b.build(), HttpResponse.BodyHandlers.ofString());
  // Parse with your JSON library of choice; read .data on ok, .error.code otherwise.
  return res.body();
}
def api(method, path, body = nil, extra = {})
  uri = URI("#{API}#{path}")
  klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }[method]
  req = klass.new(uri)
  req["Authorization"] = "Bearer #{TOKEN}"
  extra.each { |k, v| req[k] = v }
  if body
    req["Content-Type"] = "application/json"
    req.body = body.to_json
  end
  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
  env = JSON.parse(res.body)
  raise "#{env['error']['code']}: #{env['error']['message']}" unless env["ok"]
  env["data"]
end
<?php
function api(string $method, string $path, $body = null, array $extra = []) {
  global $token;
  $headers = array_merge(["Authorization: Bearer {$token}"], $extra);
  $opts = [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method];
  if ($body !== null) {
    $headers[] = "Content-Type: application/json";
    $opts[CURLOPT_POSTFIELDS] = json_encode($body);
  }
  $opts[CURLOPT_HTTPHEADER] = $headers;
  $ch = curl_init(API . $path);
  curl_setopt_array($ch, $opts);
  $env = json_decode(curl_exec($ch), true);
  if (empty($env["ok"])) {
    throw new RuntimeException($env["error"]["code"] . ": " . $env["error"]["message"]);
  }
  return $env["data"];
}
static async Task<JsonElement> Api(string method, string path, object? body = null,
                                    Dictionary<string, string>? extra = null) {
  using var req = new HttpRequestMessage(new HttpMethod(method), API + path);
  req.Headers.Add("Authorization", "Bearer " + token);
  if (extra != null) foreach (var kv in extra) req.Headers.Add(kv.Key, kv.Value);
  if (body != null) req.Content = JsonContent.Create(body);
  var res = await http.SendAsync(req);
  var env = await res.Content.ReadFromJsonAsync<JsonElement>();
  if (!env.GetProperty("ok").GetBoolean()) {
    var e = env.GetProperty("error");
    throw new Exception(e.GetProperty("code").GetString() + ": " +
                        e.GetProperty("message").GetString());
  }
  return env.GetProperty("data");
}

3. Who am I, and what is my balance

/me returns subject_type ("user" or "guest") and credits. Compare the balance against /estimate before running — that is what turns a 402 after submit into a clear message before it.

GET /me
curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq '.data'
# {"subject_type":"user","credits":184320,...}
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
  SubjectType string `json:"subject_type"`
  Credits     int64  `json:"credits"`
}
if err := api("GET", "/me", nil, nil, &me); err != nil {
  panic(err)
}
fmt.Println(me.SubjectType, me.Credits)
System.out.println(api("GET", "/me", null, null));
// {"ok":true,"data":{"subject_type":"user","credits":184320}}
me = api("GET", "/me")
puts "#{me['subject_type']} #{me['credits']}"
<?php
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"], "\n";
var me = await Api("GET", "/me");
Console.WriteLine($"{me.GetProperty("subject_type").GetString()} " +
                  $"{me.GetProperty("credits").GetInt64()}");
Credits are integer hundredths of a cent: 10000 credits is one US dollar. The app renders them as dollars, and so should you.

4. Price the run — free, no job, no charge

/estimate takes the same input object as /run and returns what a run would reserve. It creates no job and costs nothing, so it is also the cheapest way to prove your token, your input shape and the app's model binding are all valid.

This app is bound to the model alias gpt-terra, which currently resolves to gpt-5.6-terra, at markup_bps: 1000 (a 10% publisher markup). The response fields worth reading:

POST /estimate

The input object is exactly what the page submits. dump_output is the only required field; everything else has a default. Note the tabs inside dump_output are real tab characters, written here as \t because that is how JSON encodes them.

cat > input.json <<'JSON'
{
  "case": {
    "name": "WKSTN-4471",
    "os_hint": "windows",
    "acquisition": "winpmem",
    "scenario": "ransomware"
  },
  "dump_output": "$ vol -f mem.raw windows.pslist\nPID\tPPID\tImageFileName\tOffset(V)\n4\t0\tSystem\t0x8e0f4b8d3900\n624\t524\tservices.exe\t0x8e0f4b9b0640\n3288\t3120\tsvchost.exe\t0x8e0f4d21a080\n\n$ vol -f mem.raw windows.malfind\nPID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes\n3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found\n",
  "analyst_note": "EDR fired on PID 3288 at 09:40.",
  "prescan_facts": null,
  "current_datetime": "2026-08-12T14:00:00+00:00 (Wednesday)"
}
JSON

curl -s -X POST "$API/estimate" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @input.json | jq '.data | {model, model_alias, markup_bps, hold_credits, min_credits}'
DUMP = (
    "$ vol -f mem.raw windows.pslist\n"
    "PID\tPPID\tImageFileName\tOffset(V)\n"
    "4\t0\tSystem\t0x8e0f4b8d3900\n"
    "624\t524\tservices.exe\t0x8e0f4b9b0640\n"
    "3288\t3120\tsvchost.exe\t0x8e0f4d21a080\n\n"
    "$ vol -f mem.raw windows.malfind\n"
    "PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes\n"
    "3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found\n"
)

payload = {
    "case": {"name": "WKSTN-4471", "os_hint": "windows",
             "acquisition": "winpmem", "scenario": "ransomware"},
    "dump_output": DUMP,
    "analyst_note": "EDR fired on PID 3288 at 09:40.",
    "prescan_facts": None,
    "current_datetime": "2026-08-12T14:00:00+00:00 (Wednesday)",
}

est = api("POST", "/estimate", payload)
print(est["model"], est["model_alias"], est["markup_bps"])
print("reserved:", est["hold_credits"], "floor:", est["min_credits"])
assert est["model_alias"] == "gpt-terra"
const DUMP =
  "$ vol -f mem.raw windows.pslist\n" +
  "PID\tPPID\tImageFileName\tOffset(V)\n" +
  "4\t0\tSystem\t0x8e0f4b8d3900\n" +
  "624\t524\tservices.exe\t0x8e0f4b9b0640\n" +
  "3288\t3120\tsvchost.exe\t0x8e0f4d21a080\n\n" +
  "$ vol -f mem.raw windows.malfind\n" +
  "PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes\n" +
  "3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found\n";

const payload = {
  case: { name: "WKSTN-4471", os_hint: "windows",
          acquisition: "winpmem", scenario: "ransomware" },
  dump_output: DUMP,
  analyst_note: "EDR fired on PID 3288 at 09:40.",
  prescan_facts: null,
  current_datetime: "2026-08-12T14:00:00+00:00 (Wednesday)",
};

const est = await api("POST", "/estimate", payload);
console.log(est.model, est.model_alias, est.markup_bps);
console.log("reserved:", est.hold_credits, "floor:", est.min_credits);
dump := "$ vol -f mem.raw windows.pslist\n" +
  "PID\tPPID\tImageFileName\tOffset(V)\n" +
  "4\t0\tSystem\t0x8e0f4b8d3900\n" +
  "3288\t3120\tsvchost.exe\t0x8e0f4d21a080\n\n" +
  "$ vol -f mem.raw windows.malfind\n" +
  "PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes\n" +
  "3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found\n"

payload := map[string]any{
  "case": map[string]string{
    "name": "WKSTN-4471", "os_hint": "windows",
    "acquisition": "winpmem", "scenario": "ransomware",
  },
  "dump_output":      dump,
  "analyst_note":     "EDR fired on PID 3288 at 09:40.",
  "current_datetime": "2026-08-12T14:00:00+00:00 (Wednesday)",
}

var est struct {
  Model      string `json:"model"`
  ModelAlias string `json:"model_alias"`
  MarkupBps  int    `json:"markup_bps"`
  Hold       int64  `json:"hold_credits"`
  Min        int64  `json:"min_credits"`
}
if err := api("POST", "/estimate", payload, nil, &est); err != nil {
  panic(err)
}
fmt.Println(est.Model, est.ModelAlias, est.MarkupBps, est.Hold, est.Min)
String dump = String.join("\n",
    "$ vol -f mem.raw windows.pslist",
    "PID\tPPID\tImageFileName\tOffset(V)",
    "4\t0\tSystem\t0x8e0f4b8d3900",
    "3288\t3120\tsvchost.exe\t0x8e0f4d21a080",
    "",
    "$ vol -f mem.raw windows.malfind",
    "PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes",
    "3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found");

// Build the JSON with your library of choice; the shape is:
//   {"case":{...},"dump_output":"...","analyst_note":"...","current_datetime":"..."}
String body = mapper.writeValueAsString(Map.of(
    "case", Map.of("name", "WKSTN-4471", "os_hint", "windows",
                   "acquisition", "winpmem", "scenario", "ransomware"),
    "dump_output", dump,
    "analyst_note", "EDR fired on PID 3288 at 09:40.",
    "current_datetime", "2026-08-12T14:00:00+00:00 (Wednesday)"));

System.out.println(api("POST", "/estimate", body, null));
// data.model == "gpt-5.6-terra", data.model_alias == "gpt-terra", data.markup_bps == 1000
DUMP = [
  "$ vol -f mem.raw windows.pslist",
  "PID\tPPID\tImageFileName\tOffset(V)",
  "4\t0\tSystem\t0x8e0f4b8d3900",
  "3288\t3120\tsvchost.exe\t0x8e0f4d21a080",
  "",
  "$ vol -f mem.raw windows.malfind",
  "PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes",
  "3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found"
].join("\n")

payload = {
  case: { name: "WKSTN-4471", os_hint: "windows",
          acquisition: "winpmem", scenario: "ransomware" },
  dump_output: DUMP,
  analyst_note: "EDR fired on PID 3288 at 09:40.",
  current_datetime: "2026-08-12T14:00:00+00:00 (Wednesday)"
}

est = api("POST", "/estimate", payload)
puts "#{est['model']} #{est['model_alias']} #{est['markup_bps']}"
puts "reserved: #{est['hold_credits']} floor: #{est['min_credits']}"
<?php
$dump = implode("\n", [
  '$ vol -f mem.raw windows.pslist',
  "PID\tPPID\tImageFileName\tOffset(V)",
  "4\t0\tSystem\t0x8e0f4b8d3900",
  "3288\t3120\tsvchost.exe\t0x8e0f4d21a080",
  "",
  '$ vol -f mem.raw windows.malfind',
  "PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes",
  "3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found",
]);

$payload = [
  "case" => ["name" => "WKSTN-4471", "os_hint" => "windows",
             "acquisition" => "winpmem", "scenario" => "ransomware"],
  "dump_output" => $dump,
  "analyst_note" => "EDR fired on PID 3288 at 09:40.",
  "current_datetime" => "2026-08-12T14:00:00+00:00 (Wednesday)",
];

$est = api("POST", "/estimate", $payload);
echo $est["model"], " ", $est["model_alias"], " ", $est["markup_bps"], "\n";
echo "reserved: ", $est["hold_credits"], " floor: ", $est["min_credits"], "\n";
var dump = string.Join("\n", new[] {
  "$ vol -f mem.raw windows.pslist",
  "PID\tPPID\tImageFileName\tOffset(V)",
  "4\t0\tSystem\t0x8e0f4b8d3900",
  "3288\t3120\tsvchost.exe\t0x8e0f4d21a080",
  "",
  "$ vol -f mem.raw windows.malfind",
  "PID\tProcess\tStart VPN\tEnd VPN\tTag\tProtection\tNotes",
  "3288\tsvchost.exe\t0x1f0000\t0x1f7fff\tVadS\tPAGE_EXECUTE_READWRITE\tMZ header found"
});

var payload = new {
  @case = new { name = "WKSTN-4471", os_hint = "windows",
                acquisition = "winpmem", scenario = "ransomware" },
  dump_output = dump,
  analyst_note = "EDR fired on PID 3288 at 09:40.",
  current_datetime = "2026-08-12T14:00:00+00:00 (Wednesday)"
};

var est = await Api("POST", "/estimate", payload);
Console.WriteLine(est.GetProperty("model_alias").GetString());   // gpt-terra
Console.WriteLine(est.GetProperty("hold_credits").GetInt64());
The case key is a reserved word in some languages. C# escapes it as @case; in Java or Go build the map explicitly, as above.

5. Run it, then poll the job

/run is metered. It returns a job_id immediately; poll /jobs/{job_id} until status is terminal (succeeded, failed or canceled). The report text is in output.output on success, alongside charged_credits — the amount actually billed, usually well under the hold — and truncated.

Always send an Idempotency-Key. Derive it from the input, not from a random value or a clock: if a connection drops after the platform accepted the run, your retry must carry the same key so it joins the existing job instead of starting a second billable one. The app uses ram-triage:{hash of dump_output + case + analyst_note}:a{attempt}, and its automatic reformat-retry reuses the same hash with the attempt number bumped, so a malformed first reply cannot double-charge.

POST /run GET /jobs/{job_id}
KEY="ram-triage:$(shasum -a 256 input.json | cut -c1-16):a1"

JOB=$(curl -s -X POST "$API/run" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  --data-binary @input.json | jq -r '.data.job_id')

while :; do
  J=$(curl -s "$API/jobs/$JOB" -H "Authorization: Bearer $TOKEN" | jq '.data')
  S=$(echo "$J" | jq -r '.status')
  [ "$S" = "succeeded" ] || [ "$S" = "failed" ] || [ "$S" = "canceled" ] && break
  sleep 2
done

echo "$J" | jq -r '.output.output' > report.json
echo "$J" | jq '{status, charged_credits, truncated}'
import hashlib, json, time

def idem_key(payload, attempt=1):
    basis = json.dumps({
        "d": payload["dump_output"],
        "c": payload["case"],
        "n": payload.get("analyst_note", ""),
    }, sort_keys=True).encode()
    return "ram-triage:%s:a%d" % (hashlib.sha256(basis).hexdigest()[:16], attempt)

job = api("POST", "/run", payload, {"Idempotency-Key": idem_key(payload)})
job_id = job["job_id"]

while True:
    j = api("GET", "/jobs/" + job_id)
    if j["status"] in ("succeeded", "failed", "canceled"):
        break
    time.sleep(2)

if j["status"] != "succeeded":
    raise RuntimeError("run " + j["status"])

report = json.loads(j["output"]["output"])
print("charged:", j["charged_credits"], "truncated:", j.get("truncated"))
print(report["posture"], "-", report["verdict"])
import { createHash } from "node:crypto";

function idemKey(payload, attempt = 1) {
  const basis = JSON.stringify({
    d: payload.dump_output, c: payload.case, n: payload.analyst_note || "",
  });
  const h = createHash("sha256").update(basis).digest("hex").slice(0, 16);
  return `ram-triage:${h}:a${attempt}`;
}

const { job_id } = await api("POST", "/run", payload,
  { "Idempotency-Key": idemKey(payload) });

let j;
for (;;) {
  j = await api("GET", "/jobs/" + job_id);
  if (["succeeded", "failed", "canceled"].includes(j.status)) break;
  await new Promise((r) => setTimeout(r, 2000));
}
if (j.status !== "succeeded") throw new Error("run " + j.status);

const report = JSON.parse(j.output.output);
console.log("charged:", j.charged_credits, "truncated:", j.truncated);
console.log(report.posture, "-", report.verdict);
func idemKey(dump string, attempt int) string {
  sum := sha256.Sum256([]byte(dump))
  return fmt.Sprintf("ram-triage:%x:a%d", sum[:8], attempt)
}

var started struct{ JobID string `json:"job_id"` }
extra := map[string]string{"Idempotency-Key": idemKey(dump, 1)}
if err := api("POST", "/run", payload, extra, &started); err != nil {
  panic(err)
}

var job struct {
  Status   string `json:"status"`
  Charged  int64  `json:"charged_credits"`
  Truncated bool  `json:"truncated"`
  Output   struct{ Output string `json:"output"` } `json:"output"`
}
for {
  if err := api("GET", "/jobs/"+started.JobID, nil, nil, &job); err != nil {
    panic(err)
  }
  if job.Status == "succeeded" || job.Status == "failed" || job.Status == "canceled" {
    break
  }
  time.Sleep(2 * time.Second)
}
fmt.Println(job.Status, job.Charged, job.Truncated)
fmt.Println(job.Output.Output) // the JSON report
MessageDigest md = MessageDigest.getInstance("SHA-256");
String hash = HexFormat.of().formatHex(md.digest(dump.getBytes())).substring(0, 16);
String key = "ram-triage:" + hash + ":a1";

String started = api("POST", "/run", body, Map.of("Idempotency-Key", key));
String jobId = mapper.readTree(started).at("/data/job_id").asText();

JsonNode job;
while (true) {
  job = mapper.readTree(api("GET", "/jobs/" + jobId, null, null)).get("data");
  String st = job.get("status").asText();
  if (st.equals("succeeded") || st.equals("failed") || st.equals("canceled")) break;
  Thread.sleep(2000);
}
System.out.println(job.get("status").asText() + " " + job.get("charged_credits").asLong());
System.out.println(job.at("/output/output").asText()); // the JSON report
require "digest"

def idem_key(payload, attempt = 1)
  basis = { d: payload[:dump_output], c: payload[:case],
            n: payload[:analyst_note].to_s }.to_json
  "ram-triage:#{Digest::SHA256.hexdigest(basis)[0, 16]}:a#{attempt}"
end

started = api("POST", "/run", payload, { "Idempotency-Key" => idem_key(payload) })
job_id = started["job_id"]

loop do
  @job = api("GET", "/jobs/#{job_id}")
  break if %w[succeeded failed canceled].include?(@job["status"])
  sleep 2
end
raise "run #{@job['status']}" unless @job["status"] == "succeeded"

report = JSON.parse(@job["output"]["output"])
puts "charged: #{@job['charged_credits']} truncated: #{@job['truncated']}"
puts "#{report['posture']} - #{report['verdict']}"
<?php
function idem_key(array $payload, int $attempt = 1): string {
  $basis = json_encode([
    "d" => $payload["dump_output"],
    "c" => $payload["case"],
    "n" => $payload["analyst_note"] ?? "",
  ]);
  return "ram-triage:" . substr(hash("sha256", $basis), 0, 16) . ":a{$attempt}";
}

$started = api("POST", "/run", $payload,
               ["Idempotency-Key: " . idem_key($payload)]);
$jobId = $started["job_id"];

do {
  $job = api("GET", "/jobs/{$jobId}");
  if (in_array($job["status"], ["succeeded", "failed", "canceled"], true)) break;
  sleep(2);
} while (true);

$report = json_decode($job["output"]["output"], true);
echo "charged: ", $job["charged_credits"], "\n";
echo $report["posture"], " - ", $report["verdict"], "\n";
using System.Security.Cryptography;
using System.Text;

static string IdemKey(string dump, int attempt = 1) {
  var h = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(dump)));
  return $"ram-triage:{h[..16].ToLowerInvariant()}:a{attempt}";
}

var started = await Api("POST", "/run", payload,
    new Dictionary<string, string> { ["Idempotency-Key"] = IdemKey(dump) });
var jobId = started.GetProperty("job_id").GetString();

JsonElement job;
while (true) {
  job = await Api("GET", "/jobs/" + jobId);
  var st = job.GetProperty("status").GetString();
  if (st is "succeeded" or "failed" or "canceled") break;
  await Task.Delay(2000);
}

var reportText = job.GetProperty("output").GetProperty("output").GetString();
Console.WriteLine(job.GetProperty("charged_credits").GetInt64());
Console.WriteLine(reportText);
If truncated comes back true, the balance sat between min_credits and hold_credits and the output cap was reduced. The report you got is real but short — render what parsed and say so, rather than presenting a clipped answer as complete.

6. Stream it instead

/run-stream is the same billable operation delivered as Server-Sent Events, so a long triage shows progress instead of a spinner. Events carry event: job when the job is accepted, event: delta with a {"text":"..."} chunk, and event: done with the settled totals. Send the same Idempotency-Key you would send to /run.

POST /run-stream
curl -N -s -X POST "$API/run-stream" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "Idempotency-Key: $KEY" \
  --data-binary @input.json
import json, urllib.request

req = urllib.request.Request(
    API + "/run-stream",
    data=json.dumps(payload).encode(),
    headers={
        "Authorization": "Bearer " + TOKEN,
        "Content-Type": "application/json",
        "Accept": "text/event-stream",
        "Idempotency-Key": idem_key(payload),
    },
    method="POST",
)

raw, event = "", None
with urllib.request.urlopen(req) as r:
    for line in r:
        line = line.decode().rstrip("\n")
        if line.startswith("event: "):
            event = line[7:]
        elif line.startswith("data: "):
            data = json.loads(line[6:])
            if event == "delta":
                raw += data.get("text", "")
                print("\rreceived %d chars" % len(raw), end="")
            elif event == "done":
                print("\ncharged:", data.get("charged_credits"))

report = json.loads(raw[raw.index("{"): raw.rindex("}") + 1])
const res = await fetch(API + "/run-stream", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + TOKEN,
    "Content-Type": "application/json",
    Accept: "text/event-stream",
    "Idempotency-Key": idemKey(payload),
  },
  body: JSON.stringify(payload),
});

const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null;

for (;;) {
  const { done, value } = await reader.read();
  if (done) break;
  buf += dec.decode(value, { stream: true });
  const lines = buf.split("\n");
  buf = lines.pop();
  for (const line of lines) {
    if (line.startsWith("event: ")) event = line.slice(7);
    else if (line.startsWith("data: ")) {
      const data = JSON.parse(line.slice(6));
      if (event === "delta") raw += data.text || "";
      else if (event === "done") console.log("charged:", data.charged_credits);
    }
  }
}

const report = JSON.parse(raw.slice(raw.indexOf("{"), raw.lastIndexOf("}") + 1));
b, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Idempotency-Key", idemKey(dump, 1))

res, err := http.DefaultClient.Do(req)
if err != nil {
  panic(err)
}
defer res.Body.Close()

var raw strings.Builder
event := ""
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
for sc.Scan() {
  line := sc.Text()
  switch {
  case strings.HasPrefix(line, "event: "):
    event = strings.TrimPrefix(line, "event: ")
  case strings.HasPrefix(line, "data: "):
    var d struct {
      Text    string `json:"text"`
      Charged int64  `json:"charged_credits"`
    }
    json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &d)
    if event == "delta" {
      raw.WriteString(d.Text)
    } else if event == "done" {
      fmt.Println("charged:", d.Charged)
    }
  }
}
fmt.Println(raw.String())
HttpRequest req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
    .header("Authorization", "Bearer " + token)
    .header("Content-Type", "application/json")
    .header("Accept", "text/event-stream")
    .header("Idempotency-Key", key)
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

StringBuilder raw = new StringBuilder();
String[] event = { null };
HttpClient.newHttpClient()
    .send(req, HttpResponse.BodyHandlers.ofLines())
    .body()
    .forEach(line -> {
      if (line.startsWith("event: ")) {
        event[0] = line.substring(7);
      } else if (line.startsWith("data: ")) {
        try {
          JsonNode d = mapper.readTree(line.substring(6));
          if ("delta".equals(event[0])) raw.append(d.path("text").asText());
          else if ("done".equals(event[0]))
            System.out.println("charged: " + d.path("charged_credits").asLong());
        } catch (Exception ignored) { }
      }
    });
System.out.println(raw);
uri = URI("#{API}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Accept"] = "text/event-stream"
req["Idempotency-Key"] = idem_key(payload)
req.body = payload.to_json

raw = +""
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(req) do |res|
    res.read_body do |chunk|
      chunk.each_line do |line|
        line = line.chomp
        if line.start_with?("event: ")
          event = line[7..]
        elsif line.start_with?("data: ")
          data = JSON.parse(line[6..])
          raw << data.fetch("text", "") if event == "delta"
          puts "charged: #{data['charged_credits']}" if event == "done"
        end
      end
    end
  end
end

report = JSON.parse(raw[raw.index("{")..raw.rindex("}")])
<?php
$raw = "";
$event = null;

$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer {$token}",
    "Content-Type: application/json",
    "Accept: text/event-stream",
    "Idempotency-Key: " . idem_key($payload),
  ],
  CURLOPT_POSTFIELDS => json_encode($payload),
  CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
    foreach (explode("\n", $chunk) as $line) {
      if (str_starts_with($line, "event: ")) {
        $event = substr($line, 7);
      } elseif (str_starts_with($line, "data: ")) {
        $d = json_decode(substr($line, 6), true);
        if ($event === "delta") $raw .= $d["text"] ?? "";
        if ($event === "done") echo "charged: ", $d["charged_credits"] ?? 0, "\n";
      }
    }
    return strlen($chunk);
  },
]);
curl_exec($ch);

$start = strpos($raw, "{");
$report = json_decode(substr($raw, $start, strrpos($raw, "}") - $start + 1), true);
using var req = new HttpRequestMessage(HttpMethod.Post, API + "/run-stream");
req.Headers.Add("Authorization", "Bearer " + token);
req.Headers.Add("Accept", "text/event-stream");
req.Headers.Add("Idempotency-Key", IdemKey(dump));
req.Content = JsonContent.Create(payload);

using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var stream = await res.Content.ReadAsStreamAsync();
using var sr = new StreamReader(stream);

var raw = new StringBuilder();
string? evt = null, line;
while ((line = await sr.ReadLineAsync()) != null) {
  if (line.StartsWith("event: ")) {
    evt = line[7..];
  } else if (line.StartsWith("data: ")) {
    var d = JsonDocument.Parse(line[6..]).RootElement;
    if (evt == "delta" && d.TryGetProperty("text", out var t))
      raw.Append(t.GetString());
    else if (evt == "done" && d.TryGetProperty("charged_credits", out var c))
      Console.WriteLine($"charged: {c.GetInt64()}");
  }
}
Console.WriteLine(raw.ToString());
Keep whatever arrived if the stream dies mid-flight. The app recovers partial reports by counting which top-level keys made it ("suspicious_processes", "iocs", "next_commands" and so on) and showing the raw text rather than discarding the run you already paid for.

7. The result contract

The model returns one JSON object as text. Strip any code fence, take the substring from the first { to the last }, and parse — that is exactly what the app's parseResult() does. Its normalize() then coerces every field and throws on four conditions, which are therefore the only hard requirements:

Everything else degrades safely: unknown enum values fall back (posture to suspicious-needs-followup, a severity to medium, an iocs[].type to filename, an injection_findings[].verdict to inconclusive), missing arrays become empty arrays, and a pid given as a string is coerced to a number. Empty arrays are valid and are what a clean capture should produce.

FieldTypeNotes
case_namestringDefaults to "Memory triage".
postureenumOne of compromise-confirmed, suspicious-needs-followup, no-evidence-found.
verdictstringRequired. One actionable sentence.
exec_summarystringRequired. What the capture shows and does not show.
assumptions, open_questions, containment_steps, next_stepsstring[]Plain lists.
suspicious_processesobject[]pid, ppid (numbers), name, path, severity (critical|high|medium|low), technique, why, evidence_quote.
injection_findingsobject[]pid, process, region, protection, verdict (injected|likely-benign|inconclusive), why.
network_indicatorsobject[]pid, process, local, remote, state, severity, why.
persistenceobject[]mechanism (dropped if empty), detail, severity, why.
iocsobject[]type (ip|domain|path|filename|hash|registry|mutex|port), value (dropped if empty), context, confidence (high|medium|low).
timelineobject[]when (defaults to "unknown"), what (dropped if empty), source.
next_commandsobject[]Required, non-empty. command, plugin, why.
coverage_checkobjectaddressed (string[] of prescan flag ids), not_addressed ([{id, why}]), extra_notes (string).
summarystringOne closing paragraph.
# report.json was written in step 5. Pull the parts you want:
jq -r '.posture, .verdict' report.json

jq -r '.suspicious_processes[]
       | "\(.severity)\t\(.pid)\t\(.name)\t\(.technique)"' report.json

# indicators as a CSV blocklist
jq -r '["type","value","confidence","context"], (.iocs[]
       | [.type, .value, .confidence, .context]) | @csv' report.json > iocs.csv

# the follow-up commands, ready to paste into a shell
jq -r '.next_commands[].command' report.json
REQUIRED_ENUMS = {"compromise-confirmed", "suspicious-needs-followup", "no-evidence-found"}

def parse_report(text):
    t = text.strip()
    if t.startswith("```"):
        t = t.split("\n", 1)[1].rsplit("```", 1)[0]
    obj = json.loads(t[t.index("{"): t.rindex("}") + 1])
    for field in ("verdict", "exec_summary"):
        if not str(obj.get(field, "")).strip():
            raise ValueError(field + " must be a non-empty string")
    if not obj.get("next_commands"):
        raise ValueError("next_commands must contain at least one command")
    if obj.get("posture") not in REQUIRED_ENUMS:
        obj["posture"] = "suspicious-needs-followup"
    return obj

report = parse_report(j["output"]["output"])
for p in report["suspicious_processes"]:
    print(p["severity"], p["pid"], p["name"], "-", p["technique"])
print(len(report["iocs"]), "indicators;", len(report["next_commands"]), "commands")
const POSTURES = ["compromise-confirmed", "suspicious-needs-followup", "no-evidence-found"];

function parseReport(text) {
  let t = String(text).trim().replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "");
  const i = t.indexOf("{"), k = t.lastIndexOf("}");
  if (i < 0 || k <= i) throw new Error("no JSON object found");
  const obj = JSON.parse(t.slice(i, k + 1));
  if (!String(obj.verdict || "").trim()) throw new Error("verdict must be non-empty");
  if (!String(obj.exec_summary || "").trim()) throw new Error("exec_summary must be non-empty");
  if (!(obj.next_commands || []).length) throw new Error("next_commands must be non-empty");
  if (!POSTURES.includes(obj.posture)) obj.posture = "suspicious-needs-followup";
  return obj;
}

const report = parseReport(j.output.output);
report.suspicious_processes.forEach((p) =>
  console.log(p.severity, p.pid, p.name, "-", p.technique));
type Process struct {
  PID       int    `json:"pid"`
  PPID      int    `json:"ppid"`
  Name      string `json:"name"`
  Path      string `json:"path"`
  Severity  string `json:"severity"`
  Technique string `json:"technique"`
  Why       string `json:"why"`
  Evidence  string `json:"evidence_quote"`
}

type IOC struct {
  Type       string `json:"type"`
  Value      string `json:"value"`
  Context    string `json:"context"`
  Confidence string `json:"confidence"`
}

type Report struct {
  CaseName    string    `json:"case_name"`
  Posture     string    `json:"posture"`
  Verdict     string    `json:"verdict"`
  ExecSummary string    `json:"exec_summary"`
  Processes   []Process `json:"suspicious_processes"`
  IOCs        []IOC     `json:"iocs"`
  NextCommands []struct {
    Command string `json:"command"`
    Plugin  string `json:"plugin"`
    Why     string `json:"why"`
  } `json:"next_commands"`
  Summary string `json:"summary"`
}

text := job.Output.Output
var report Report
if err := json.Unmarshal([]byte(text[strings.Index(text, "{"):strings.LastIndex(text, "}")+1]), &report); err != nil {
  panic(err)
}
if report.Verdict == "" || report.ExecSummary == "" || len(report.NextCommands) == 0 {
  panic("report is missing a required field")
}
for _, p := range report.Processes {
  fmt.Printf("%s\t%d\t%s\t%s\n", p.Severity, p.PID, p.Name, p.Technique)
}
String text = job.at("/output/output").asText();
String json = text.substring(text.indexOf('{'), text.lastIndexOf('}') + 1);
JsonNode report = mapper.readTree(json);

if (report.path("verdict").asText().isBlank()
    || report.path("exec_summary").asText().isBlank()
    || !report.path("next_commands").elements().hasNext()) {
  throw new IllegalStateException("report is missing a required field");
}

System.out.println(report.get("posture").asText() + " - " + report.get("verdict").asText());
for (JsonNode p : report.path("suspicious_processes")) {
  System.out.printf("%s\t%d\t%s\t%s%n",
      p.path("severity").asText(), p.path("pid").asInt(),
      p.path("name").asText(), p.path("technique").asText());
}
POSTURES = %w[compromise-confirmed suspicious-needs-followup no-evidence-found].freeze

def parse_report(text)
  t = text.strip.sub(/\A```[a-z]*\s*/i, "").sub(/```\s*\z/, "")
  obj = JSON.parse(t[t.index("{")..t.rindex("}")])
  %w[verdict exec_summary].each do |f|
    raise "#{f} must be a non-empty string" if obj[f].to_s.strip.empty?
  end
  raise "next_commands must be non-empty" if Array(obj["next_commands"]).empty?
  obj["posture"] = "suspicious-needs-followup" unless POSTURES.include?(obj["posture"])
  obj
end

report = parse_report(@job["output"]["output"])
report["suspicious_processes"].each do |p|
  puts [p["severity"], p["pid"], p["name"], p["technique"]].join("\t")
end
<?php
const POSTURES = ["compromise-confirmed", "suspicious-needs-followup", "no-evidence-found"];

function parse_report(string $text): array {
  $t = trim(preg_replace('/^```[a-z]*\s*/i', "", $text));
  $t = preg_replace('/```\s*$/', "", $t);
  $start = strpos($t, "{");
  $obj = json_decode(substr($t, $start, strrpos($t, "}") - $start + 1), true);
  foreach (["verdict", "exec_summary"] as $f) {
    if (trim($obj[$f] ?? "") === "") throw new RuntimeException("$f must be non-empty");
  }
  if (empty($obj["next_commands"])) throw new RuntimeException("next_commands must be non-empty");
  if (!in_array($obj["posture"] ?? "", POSTURES, true)) {
    $obj["posture"] = "suspicious-needs-followup";
  }
  return $obj;
}

$report = parse_report($job["output"]["output"]);
foreach ($report["suspicious_processes"] as $p) {
  echo implode("\t", [$p["severity"], $p["pid"], $p["name"], $p["technique"]]), "\n";
}
static readonly string[] Postures =
  { "compromise-confirmed", "suspicious-needs-followup", "no-evidence-found" };

static JsonElement ParseReport(string text) {
  var t = text.Trim();
  if (t.StartsWith("```")) t = t[(t.IndexOf('\n') + 1)..t.LastIndexOf("```")];
  var i = t.IndexOf('{');
  var doc = JsonDocument.Parse(t[i..(t.LastIndexOf('}') + 1)]);
  var root = doc.RootElement;
  if (string.IsNullOrWhiteSpace(root.GetProperty("verdict").GetString()))
    throw new Exception("verdict must be non-empty");
  if (root.GetProperty("next_commands").GetArrayLength() == 0)
    throw new Exception("next_commands must be non-empty");
  return root;
}

var report = ParseReport(reportText!);
foreach (var p in report.GetProperty("suspicious_processes").EnumerateArray()) {
  Console.WriteLine($"{p.GetProperty("severity").GetString()}\t" +
                    $"{p.GetProperty("pid").GetInt32()}\t" +
                    $"{p.GetProperty("name").GetString()}");
}
Two prohibitions the prompt enforces, worth asserting in your own client: every evidence_quote is a verbatim substring of the dump_output you sent, and no pid appears that is absent from it. The app checks both and surfaces violations rather than trusting the answer. If you drive the API directly, do the same — it is a two-line check and it catches the failure mode that matters.

8. Send prescan_facts

The browser app parses the Volatility tables client-side before running anything and passes the result in as prescan_facts. It is ground truth produced by a deterministic parser, not by a model, and the prompt requires the report to reconcile every flags[].id in coverage_check — confirmed as a finding, or explicitly dismissed with a reason.

Sending null is legal and the run still works; you simply lose the audit. If you can compute even a few facts, send them. The shape:

jq '.prescan_facts = {
  "stats": {"processes":3,"hidden":0,"connections":0,"external":0,"rwx":1,"iocs":0},
  "plugins": ["pslist","malfind"],
  "missing_plugins": ["psscan","cmdline","netscan"],
  "flags": [{"id":"inject:pid3288-0","label":"svchost.exe (PID 3288) 0x1f0000",
             "detail":"PAGE_EXECUTE_READWRITE, MZ header present in the region."}],
  "hidden_pids": [],
  "clipped": {"chars_removed":0,"sections":[]}
}' input.json > input-with-facts.json
payload["prescan_facts"] = {
    "stats": {"processes": 3, "hidden": 0, "connections": 0,
              "external": 0, "rwx": 1, "iocs": 0},
    "plugins": ["pslist", "malfind"],
    "missing_plugins": ["psscan", "cmdline", "netscan"],
    "flags": [{
        "id": "inject:pid3288-0",
        "label": "svchost.exe (PID 3288) 0x1f0000",
        "detail": "PAGE_EXECUTE_READWRITE, MZ header present in the region.",
    }],
    "hidden_pids": [],
    "clipped": {"chars_removed": 0, "sections": []},
}

report = parse_report(api("POST", "/run", payload,
                          {"Idempotency-Key": idem_key(payload)}) and "")
# Then assert the audit actually happened:
sent = {f["id"] for f in payload["prescan_facts"]["flags"]}
cov = report["coverage_check"]
handled = set(cov["addressed"]) | {n["id"] for n in cov["not_addressed"]}
assert not (sent - handled), "unreconciled flags: %s" % (sent - handled)
payload.prescan_facts = {
  stats: { processes: 3, hidden: 0, connections: 0, external: 0, rwx: 1, iocs: 0 },
  plugins: ["pslist", "malfind"],
  missing_plugins: ["psscan", "cmdline", "netscan"],
  flags: [{
    id: "inject:pid3288-0",
    label: "svchost.exe (PID 3288) 0x1f0000",
    detail: "PAGE_EXECUTE_READWRITE, MZ header present in the region.",
  }],
  hidden_pids: [],
  clipped: { chars_removed: 0, sections: [] },
};

// After the run, assert the audit happened:
const sent = new Set(payload.prescan_facts.flags.map((f) => f.id));
const cov = report.coverage_check;
const handled = new Set([...cov.addressed, ...cov.not_addressed.map((n) => n.id)]);
const missed = [...sent].filter((id) => !handled.has(id));
if (missed.length) console.warn("unreconciled flags:", missed);
payload["prescan_facts"] = map[string]any{
  "stats": map[string]int{
    "processes": 3, "hidden": 0, "connections": 0,
    "external": 0, "rwx": 1, "iocs": 0,
  },
  "plugins":         []string{"pslist", "malfind"},
  "missing_plugins": []string{"psscan", "cmdline", "netscan"},
  "flags": []map[string]string{{
    "id":     "inject:pid3288-0",
    "label":  "svchost.exe (PID 3288) 0x1f0000",
    "detail": "PAGE_EXECUTE_READWRITE, MZ header present in the region.",
  }},
  "hidden_pids": []int{},
  "clipped":     map[string]any{"chars_removed": 0, "sections": []any{}},
}
Map<String, Object> facts = Map.of(
    "stats", Map.of("processes", 3, "hidden", 0, "connections", 0,
                    "external", 0, "rwx", 1, "iocs", 0),
    "plugins", List.of("pslist", "malfind"),
    "missing_plugins", List.of("psscan", "cmdline", "netscan"),
    "flags", List.of(Map.of(
        "id", "inject:pid3288-0",
        "label", "svchost.exe (PID 3288) 0x1f0000",
        "detail", "PAGE_EXECUTE_READWRITE, MZ header present in the region.")),
    "hidden_pids", List.of(),
    "clipped", Map.of("chars_removed", 0, "sections", List.of()));

// merge `facts` into the payload map under "prescan_facts" before serialising
payload[:prescan_facts] = {
  stats: { processes: 3, hidden: 0, connections: 0, external: 0, rwx: 1, iocs: 0 },
  plugins: %w[pslist malfind],
  missing_plugins: %w[psscan cmdline netscan],
  flags: [{
    id: "inject:pid3288-0",
    label: "svchost.exe (PID 3288) 0x1f0000",
    detail: "PAGE_EXECUTE_READWRITE, MZ header present in the region."
  }],
  hidden_pids: [],
  clipped: { chars_removed: 0, sections: [] }
}

sent = payload[:prescan_facts][:flags].map { |f| f[:id] }
cov = report["coverage_check"]
handled = cov["addressed"] + cov["not_addressed"].map { |n| n["id"] }
warn "unreconciled: #{sent - handled}" unless (sent - handled).empty?
<?php
$payload["prescan_facts"] = [
  "stats" => ["processes" => 3, "hidden" => 0, "connections" => 0,
              "external" => 0, "rwx" => 1, "iocs" => 0],
  "plugins" => ["pslist", "malfind"],
  "missing_plugins" => ["psscan", "cmdline", "netscan"],
  "flags" => [[
    "id" => "inject:pid3288-0",
    "label" => "svchost.exe (PID 3288) 0x1f0000",
    "detail" => "PAGE_EXECUTE_READWRITE, MZ header present in the region.",
  ]],
  "hidden_pids" => [],
  "clipped" => ["chars_removed" => 0, "sections" => []],
];

$sent = array_column($payload["prescan_facts"]["flags"], "id");
$cov = $report["coverage_check"];
$handled = array_merge($cov["addressed"], array_column($cov["not_addressed"], "id"));
$missed = array_diff($sent, $handled);
if ($missed) fwrite(STDERR, "unreconciled: " . implode(", ", $missed) . "\n");
var facts = new {
  stats = new { processes = 3, hidden = 0, connections = 0,
                external = 0, rwx = 1, iocs = 0 },
  plugins = new[] { "pslist", "malfind" },
  missing_plugins = new[] { "psscan", "cmdline", "netscan" },
  flags = new[] { new {
    id = "inject:pid3288-0",
    label = "svchost.exe (PID 3288) 0x1f0000",
    detail = "PAGE_EXECUTE_READWRITE, MZ header present in the region."
  }},
  hidden_pids = Array.Empty<int>(),
  clipped = new { chars_removed = 0, sections = Array.Empty<object>() }
};

var payloadWithFacts = new {
  @case = payload.@case, dump_output = payload.dump_output,
  analyst_note = payload.analyst_note, prescan_facts = facts,
  current_datetime = payload.current_datetime
};

9. The cases collection

The app declares one collection, cases, with acl_read: "owner" and acl_write: "user", so each subject sees only its own records. Declared (queryable) fields:

FieldTypeEmbedded
titlestringyes
posturestringno
findingsnumberno
ran_attimestampno
hoststringyes
processesstringyes
summarystringyes

The full report round-trips inside the document under an undeclared report key: stored and returned intact, just not filterable. Two rules that cost real debugging time if missed:

Records nest under doc: read records[i].doc.title, never records[i].title. Semantic search over the embedded fields lives at /collections/cases/similar and is limited to 30 requests per minute per IP; it only ever finds records written after the embed set was declared, because the platform does not backfill vectors.

POST /collections/cases/query POST /collections/cases/similar
# confirmed compromises with at least three findings, newest first
curl -s -X POST "$API/collections/cases/query" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "where": {"posture": {"eq": "compromise-confirmed"}, "findings": {"gte": 3}},
    "sort": {"field": "ran_at", "dir": "desc"},
    "limit": 20
  }' | jq -r '.data.records[] | "\(.doc.ran_at)\t\(.doc.host)\t\(.doc.title)"'

# semantic search over title, host, processes and summary
curl -s -X POST "$API/collections/cases/similar" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text":"the workstation with the fake svchost","limit":8}' \
  | jq -r '.data.records[] | "\(.score)\t\(.doc.title)"'
res = api("POST", "/collections/cases/query", {
    "where": {"posture": {"eq": "compromise-confirmed"}, "findings": {"gte": 3}},
    "sort": {"field": "ran_at", "dir": "desc"},   # NOT order_by
    "limit": 20,
})
for rec in res["records"]:
    doc = rec["doc"]                              # records nest under `doc`
    print(doc["ran_at"], doc["host"], doc["title"], doc["findings"])

hits = api("POST", "/collections/cases/similar",
           {"text": "the workstation with the fake svchost", "limit": 8})
for rec in hits["records"]:
    print(round(rec["score"], 3), rec["doc"]["title"])
const res = await api("POST", "/collections/cases/query", {
  where: { posture: { eq: "compromise-confirmed" }, findings: { gte: 3 } },
  sort: { field: "ran_at", dir: "desc" },   // NOT order_by
  limit: 20,
});
for (const rec of res.records) {
  const doc = rec.doc;                      // records nest under `doc`
  console.log(doc.ran_at, doc.host, doc.title, doc.findings);
}

const hits = await api("POST", "/collections/cases/similar",
  { text: "the workstation with the fake svchost", limit: 8 });
hits.records.forEach((r) => console.log(r.score.toFixed(3), r.doc.title));
query := map[string]any{
  "where": map[string]any{
    "posture":  map[string]string{"eq": "compromise-confirmed"},
    "findings": map[string]int{"gte": 3},
  },
  "sort":  map[string]string{"field": "ran_at", "dir": "desc"},
  "limit": 20,
}

var page struct {
  Records []struct {
    RecordID string `json:"record_id"`
    Score    float64 `json:"score"`
    Doc      struct {
      Title    string `json:"title"`
      Host     string `json:"host"`
      Posture  string `json:"posture"`
      Findings int    `json:"findings"`
      RanAt    string `json:"ran_at"`
    } `json:"doc"`
  } `json:"records"`
}
// NOTE: the cursor is NOT in `data`. It lives at meta.pagination.next_cursor,
// alongside meta.pagination.has_more, so a paging client must read the full
// envelope rather than just the unwrapped data object.
if err := api("POST", "/collections/cases/query", query, nil, &page); err != nil {
  panic(err)
}
for _, r := range page.Records {
  fmt.Println(r.Doc.RanAt, r.Doc.Host, r.Doc.Title)
}
String query = """
    {"where":{"posture":{"eq":"compromise-confirmed"},"findings":{"gte":3}},
     "sort":{"field":"ran_at","dir":"desc"},
     "limit":20}
    """;

JsonNode page = mapper.readTree(
    api("POST", "/collections/cases/query", query, null)).get("data");

for (JsonNode rec : page.path("records")) {
  JsonNode doc = rec.path("doc");   // records nest under `doc`
  System.out.printf("%s\t%s\t%s%n",
      doc.path("ran_at").asText(), doc.path("host").asText(),
      doc.path("title").asText());
}
res = api("POST", "/collections/cases/query", {
  where: { posture: { eq: "compromise-confirmed" }, findings: { gte: 3 } },
  sort: { field: "ran_at", dir: "desc" },   # NOT order_by
  limit: 20
})

res["records"].each do |rec|
  doc = rec["doc"]                          # records nest under `doc`
  puts [doc["ran_at"], doc["host"], doc["title"], doc["findings"]].join("\t")
end

hits = api("POST", "/collections/cases/similar",
           { text: "the workstation with the fake svchost", limit: 8 })
hits["records"].each { |r| puts "#{r['score'].round(3)}\t#{r['doc']['title']}" }
<?php
$res = api("POST", "/collections/cases/query", [
  "where" => ["posture" => ["eq" => "compromise-confirmed"],
              "findings" => ["gte" => 3]],
  "sort" => ["field" => "ran_at", "dir" => "desc"],   // NOT order_by
  "limit" => 20,
]);

foreach ($res["records"] as $rec) {
  $doc = $rec["doc"];                                 // records nest under `doc`
  echo implode("\t", [$doc["ran_at"], $doc["host"], $doc["title"]]), "\n";
}

$hits = api("POST", "/collections/cases/similar",
            ["text" => "the workstation with the fake svchost", "limit" => 8]);
foreach ($hits["records"] as $r) {
  printf("%.3f\t%s\n", $r["score"], $r["doc"]["title"]);
}
var page = await Api("POST", "/collections/cases/query", new {
  where = new {
    posture = new { eq = "compromise-confirmed" },
    findings = new { gte = 3 }
  },
  sort = new { field = "ran_at", dir = "desc" },   // NOT order_by
  limit = 20
});

foreach (var rec in page.GetProperty("records").EnumerateArray()) {
  var doc = rec.GetProperty("doc");                // records nest under `doc`
  Console.WriteLine($"{doc.GetProperty("ran_at").GetString()}\t" +
                    $"{doc.GetProperty("host").GetString()}\t" +
                    $"{doc.GetProperty("title").GetString()}");
}

var hits = await Api("POST", "/collections/cases/similar",
    new { text = "the workstation with the fake svchost", limit = 8 });
foreach (var r in hits.GetProperty("records").EnumerateArray()) {
  Console.WriteLine($"{r.GetProperty("score").GetDouble():F3}\t" +
                    $"{r.GetProperty("doc").GetProperty("title").GetString()}");
}
Quotas worth knowing: 10,000 records per collection, 1,000 per owner, 64 KB per document, query limit capped at 100, 20 values per in, 10 where fields. The app trims the stored paste to about 14 KB and drops it entirely if the report alone would breach the document cap — the report is what a restore needs, the paste is a convenience.

What this app will not do

RAM Triage reads plugin output that you paste or post. It does not acquire memory, does not parse a raw image, and does not execute Volatility or anything else — the runtime is a static bundle under a strict CSP with no server. The report is instructed never to claim it ran a command, never to cite a PID absent from your input, and never to attribute activity to a named threat actor. Triage is only as wide as the plugins you pasted, and a clean result across five plugins is not a clean host.