Driving Patent Desk over HTTP
Everything the page does, you can do from a script. One endpoint does the work; the rest is
authentication and polling. The app takes one invention disclosure — prose
describing something built or conceived — and, where you have one, a numbered claim
set over that same disclosure, plus a task field naming which of four jobs to
run over the material. It returns a single JSON envelope.
https://api.skillsafe.ai/v1/app-api
The only headers on any call are Authorization: Bearer <token> and
Content-Type: application/json — plus Idempotency-Key on
/run and /run-stream. There is no slug header. The app
slug is named in exactly one place: the JSON body of POST /guest, as
{"slug":"patent-desk"}. Get a token from
the token page without opening a developer console.
The response envelope
Every endpoint returns the same wrapper. Success carries data; failure carries
error. Nothing returns a bare value, so a client can branch on the presence of
error alone.
{"ok": true, "data": { ... }}
{"ok": false, "error": {"code": "VALIDATION_ERROR", "message": "...", "details": { ... }}}
Error codes
| Code | HTTP | What it means | What to do |
|---|---|---|---|
| UNAUTHORIZED | 401 | Missing, malformed or expired token. | Mint a new one. Guest tokens expire; personal tokens outlive them. |
| FORBIDDEN | 403 | The token is valid but not for this app. | Mint the token against this app: POST /guest with {"slug":"patent-desk"}. |
| VALIDATION_ERROR | 400 | The input did not match the app's shape. | Read error.details; it names the offending field. |
| PAYMENT_REQUIRED | 402 | Balance below the run's minimum. | Call /estimate first and compare against /me. |
| RATE_LIMITED | 429 | Too many requests. | Back off and retry; never tight-loop. |
| JOB_FAILED | 200 | The job reached a terminal failed state. | Returned inside a job payload, not as an HTTP error. Check status. |
The task field comes first
Patent Desk is a four-lane app. Every run must name its lane in task,
because all four lanes share one system prompt and one model and are routed by that field alone. The
contract is to produce that lane only and never a blend of two. If task is
missing or is not one of the four ids, the model picks the closest lane, produces that lane's
contract in full, and says so by setting lane to what it chose plus
lane_inferred to true — a fallback for a malformed request, not a
feature to rely on. Send the lane explicitly.
task | What it does | Extra input | artifact.kind |
|---|---|---|---|
| disclosure | Reads the disclosure and says whether it can support a patent application yet: elements, novelty candidates, statutory class, eligibility risk, 112(a) gaps, questions for the inventor. | — | none |
| priorart | Builds the prior-art search to run before drafting: classifications, a concept matrix, queries, a protocol and knockout criteria. Produces the search, never its results. | — | markdown |
| claims | Drafts the claim set the disclosure supports, or revises a draft supplied in claims_text. | claims_text | claims |
| review112 | Reviews a pasted claim set against 35 U.S.C. 112 — definiteness, antecedent basis, dependency, support — and rewrites the defective claims. | claims_text | claims |
claims_text is required by review112 and
optional on claims, where it is an existing draft to revise rather
than replace. The natural order through the app is
disclosure → priorart → claims →
review112, and every response names what it thinks comes next in
next_lane.
Input fields
| Field | Type | Required | Notes |
|---|---|---|---|
| task | string | yes | One of the four lane ids above. |
| disclosure | string | yes | The invention disclosure itself — the one work object every lane operates on. Prose, possibly with headings, figure references and reference numerals. |
| claims_text | string | review112 only | The numbered claim set, verbatim. Optional elsewhere; on claims it is a draft to revise, keeping the user's numbering where possible. |
| jurisdiction | string | no | Defaults to US. Only US practice is in scope; name another office and the model applies US practice, says so in assumptions and names the one difference that matters most. |
| context | string | no | Free-text notes: what you think is new, what a competitor ships, what has already been published or sold, who the audience is. The priorart lane uses a named competitor or assignee here to build one of its four queries. |
| prescan | object | no | {flags, resources} — deterministic facts from the in-browser scanner that the model must reconcile. See below. |
| clip_note | string | no | Send only when the input was too long to transmit whole; it names what was cut. The page clips a disclosure from the middle, keeping the head and the tail, and clips a claim set on whole-claim boundaries, dropping claims from the end. A clipped input overrides a lane's "artifact is required": the model returns artifact.kind: "none" and posture: "blocked" rather than inventing the part it was not shown. |
| retry_note | string | no | Sent only on an automatic re-ask after a malformed reply. It quotes the parse error and restates the contract; the model answers the same lane on the same input and returns only the JSON object. Reuse the idempotency key with the attempt number appended so a retry cannot double-bill. |
The output envelope
All four lanes return exactly the same outer object. Only body is per-lane. The reply is
one JSON object and nothing else — no prose before it and no code fence around
it — and it arrives as a string in data.output.output, so you parse it yourself.
{
"lane": "review112",
"lane_inferred": false,
"invention": "a short noun phrase naming the invention, or \"unknown\"",
"title": "short human title for this run, max 70 chars",
"posture": "filing-ready | needs-work | blocked",
"verdict": "one sentence naming the single thing that decides the posture",
"statutory_classes": ["method", "apparatus"],
"summary": "3-6 sentences a drafter could paste into a file note",
"assumptions": ["..."],
"open_questions": ["..."],
"findings": [
{"id": "PD-001", "title": "short imperative title",
"severity": "critical | high | medium | low",
"area": "eligibility | novelty | claim-scope | definiteness | antecedent-basis | dependency | support | enablement | drafting | search | formalities",
"claim_no": 3, "term": "the memory",
"evidence": "the exact text from the input",
"why": "what goes wrong, naming the provision - e.g. 35 U.S.C. 112(b)",
"fix": "what to change, in one or two sentences",
"fix_text": "the corrected claim language, or \"\""}
],
"coverage_check": [
{"flag_id": "PK-ANTECEDENT-BASIS", "status": "confirmed", "finding_id": "PD-001", "note": "..."}
],
"artifact": {"kind": "none | claims | markdown", "filename": "claims.txt",
"content": "the whole artifact, or \"\" when kind is none"},
"next_lane": {"lane": "claims", "reason": "one sentence on why this is the next thing to do"},
"body": { }
}
findingsids runPD-001upward —PD-001,PD-002, sequential, no gaps, ordered most severe first. Acoverage_checkentry points at one of them byfinding_id.bodyis the only per-lane part. Everything above it has the same keys and the same vocabulary in all four lanes, so one parser handles every response and switches onlaneonly to readbody.postureuses the same three values everywhere:filing-readymeans the lane's job found nothing that must change before a practitioner reviews it,needs-workthat it did, andblockedthat something prevents the job being done at all.- Every array is present even when empty — an empty array, never
nulland never a missing key. You can index without guarding. claim_nois0when the finding is about the disclosure rather than a claim.statutory_classesdraws only onmethod,apparatus,compositionandcrm.next_lane.laneis one of the four lane ids or""when nothing sensible follows.
The prescan contract
The browser app runs a deterministic scanner over the disclosure and the claim set before every run
and passes its findings in prescan.flags, each with a stable PK-* id.
The model must return exactly one coverage_check entry per flag id sent, and
none for ids that were not sent. That is what makes the free scanner able to hold the paid
run accountable — anything unaccounted for is a defect you can detect programmatically:
sent = {f["id"] for f in payload["prescan"]["flags"]}
covered = {c["flag_id"] for c in result["coverage_check"]}
assert sent == covered, f"unreconciled: {sent - covered}; invented: {covered - sent}"
# resources are context, not findings: they must NOT appear in coverage_check
res = {r["id"] for r in payload["prescan"]["resources"]}
assert not (covered & res), f"resource reconciled as a finding: {covered & res}"
status is one of three values. confirmed — the model agrees, and
finding_id names the PD-* finding that carries it. set-aside
— it is not a real problem on this material, and note says why.
superseded — a different, larger finding covers it, and finding_id
names that one. A flag is never silently dropped.
The flag shape
Each entry in prescan.flags is
{id, label, severity, claim_no, claims, line, occurrences, terms, detail}.
| Key | Type | What it holds |
|---|---|---|
| id | string | The stable rule id, PK-*. This is the value that must come back as a coverage_check.flag_id. |
| label | string | The rule's one-line description, e.g. definite reference with no earlier indefinite antecedent. |
| severity | string | critical, high, medium or low. Flags arrive sorted by severity in that order. |
| claim_no | number | The first claim the rule fired on, or 0 when the rule is about the disclosure or the set as a whole. |
| claims | array | Every claim number the rule fired on, in the order encountered. Empty when claim_no is 0. |
| line | number | The first line the rule fired on, or 0. |
| occurrences | number | How many times the rule fired in total across the whole input. |
| terms | array | Up to twelve distinct offending terms collected across every occurrence — the relative terms, the unsupported claim terms, the definite references with no antecedent. |
| detail | string | Free text from the rule when it has a countable fact to add, e.g. 430 words, 4 independent claims, 312 words. Empty string otherwise. |
Flags are deduplicated by rule id before they are sent. One rule firing on six claims
becomes one flag: claim_no is the first of them, claims lists all six,
occurrences is 6, and terms accumulates across all six. One
coverage_check entry still covers the whole group — but an occurrences
of 6 means six places need the same change, and the contract forbids the model implying a single
site when the count says otherwise.
Where the model and the scanner disagree on a countable fact — how many claims there
are, which claim a term appears in, whether a numeral is present — the scanner is
right, because it is deterministic and the model is not. Where they disagree about whether
the flagged text is actually a problem, the model may say so; that is what set-aside is
for.
prescan.resources is context, not findings
prescan.resources is a separate array of {id, label} entries stating what
the scanner counted, not what it objects to: words of disclosure, which sections have headings, which
figures are referenced, how many distinct reference numerals, how many claims of each kind and the
maximum chain depth, which statutory classes are claimed, and which technical-effect vocabulary is
present. They need no coverage_check entry, they are not problems, and
the model is instructed not to manufacture a finding merely to mention one.
"resources": [
{"id": "PR-DISCLOSURE-WORDS", "label": "612 words of disclosure"},
{"id": "PR-SECTIONS", "label": "sections with headings: technical field, background, summary, drawings, detailed description"},
{"id": "PR-FIGURES", "label": "figures referenced: FIG. 1, FIG. 2, FIG. 4"},
{"id": "PR-NUMERALS", "label": "5 distinct reference numerals in the text"},
{"id": "PR-CLAIM-COUNTS", "label": "10 claims (3 independent, 7 dependent), max chain depth 2"},
{"id": "PR-CLASSES", "label": "statutory classes claimed: method, apparatus, crm"},
{"id": "PR-TECHNICAL", "label": "technical-effect vocabulary present: threshold, filter, sensor"}
]
Flags are filtered to the lane being run
Every rule declares which lanes it belongs to, and the scanner sends only the rules for the lane you
are about to run. The same disclosure and the same claim set therefore produce a different
flag set in each lane — a real defect that is out of lane is simply not sent, so the
model is never asked to reconcile something it is not being paid to look at. Compute
prescan per lane; do not cache one lane's flags and resend them on another.
For the disclosure and claim set bundled with the app as the worked example, the counts come out:
| Lane | Flags sent | Which rules can reach this lane |
|---|---|---|
| disclosure | 4 | The six disclosure-only rules, plus PK-TERM-NOT-IN-SPEC, PK-DISCLOSURE-THIN, PK-VAGUE-ENABLEMENT and PK-ELIGIBILITY-SIGNAL. |
| priorart | 1 | Only three: PK-DISCLOSURE-THIN, PK-VAGUE-ENABLEMENT, PK-ELIGIBILITY-SIGNAL. This lane is about the technology, not the drafting. |
| claims | 10 | The widest set: every claim-drafting rule plus the four disclosure-quality rules. |
| review112 | 9 | Every claim rule, including PK-NO-CLAIMS, which reaches this lane and no other. |
Those four numbers are one illustration, not a constant. What is constant is the shape: run the same
material through two lanes and expect two different coverage_check arrays.
The rules
Thirty-one rules, each with a fixed id and a fixed severity. d is
disclosure, p is priorart, c is
claims, r is review112.
| Rule id | Severity | Lanes | What it fires on |
|---|---|---|---|
| PK-NO-CLAIMS | critical | r | No numbered claims were found in the claim text. Claims are recognised by a leading number followed by a full stop or a bracket. |
| PK-NO-INDEPENDENT | critical | r c | Every claim refers to another claim, so the set has no root. |
| PK-NUMBERING-GAP | high | r c | Claim numbers are not consecutive from 1 — a 37 CFR 1.126 formalities objection that can break dependencies counting on the order. |
| PK-DUPLICATE-NUMBER | high | r c | The same claim number appears more than once, so any dependency on it is ambiguous. |
| PK-FORWARD-DEP | critical | r c | A claim depends on a later-numbered claim, or on itself. 37 CFR 1.75(c) requires a preceding claim. |
| PK-CIRCULAR-DEP | critical | r c | The dependency references form a cycle, so no claim in the loop has a definable scope. |
| PK-MISSING-DEP-TARGET | critical | r c | A claim depends on a claim number that does not exist. |
| PK-MULTIPLE-DEPENDENT | medium | r c | A claim refers to more than one claim. US-permissible only in the alternative, and it carries a surcharge. |
| PK-MULTI-SENTENCE | high | r c | The claim contains more than one sentence. US practice is one claim, one sentence (MPEP 608.01(m)). |
| PK-NO-TRANSITION | high | r c | No recognisable transition phrase between preamble and body, so open or closed scope is argued rather than written. |
| PK-CLOSED-TRANSITION | low | r c | consisting of — a closed transition read strictly. Confirm the narrowness is deliberate. |
| PK-ANTECEDENT-BASIS | high | r c | A definite reference with no earlier indefinite antecedent in this claim or an ancestor. 112(b), and the most common claim rejection there is. |
| PK-RELATIVE-TERM | medium | r c | A relative or approximating term of degree — substantially, about, suitable — with no yardstick in the specification. |
| PK-MEANS-PLUS-FUNCTION | medium | r c | A means-plus-function or nonce-word limitation, construed under 112(f) to the disclosed structure and its equivalents. |
| PK-REFERENCE-NUMERAL | low | r c | A reference numeral inside a claim. Permitted only in parentheses and not limiting. |
| PK-MIXED-CLASS | high | r c | One claim mixes statutory classes — method steps and apparatus structure — which is indefinite under IPXL. |
| PK-OMNIBUS | critical | r c | An omnibus claim: as described herein or as shown in the drawings. Improper in the US (MPEP 1302.04(b)). |
| PK-AND-OR | low | r c | and/or in a claim; frequently objected to as indefinite. |
| PK-LONG-CLAIM | low | r c | A claim over 250 words. Not improper, but hard to construe and easy to design around. |
| PK-DEEP-CHAIN | low | r c | A dependency chain more than four claims deep; scope narrows faster than it reads. |
| PK-THIN-DEPENDENT | medium | r c | A dependent claim adds almost nothing beyond its reference — a 112(d) rejection. |
| PK-EXCESS-INDEPENDENT | medium | r c | More than three independent claims; an excess-independent-claim fee applies. |
| PK-EXCESS-TOTAL | low | r c | More than twenty claims in total; an excess-claim fee applies. |
| PK-TERM-NOT-IN-SPEC | high | r c d | A claim term appears nowhere in the disclosure. 112(a) written description, and adding it later is new matter. |
| PK-DISCLOSURE-THIN | high | d p c | The disclosure is very short. Enablement is judged on what the specification teaches, and the gap cannot be filled after filing. |
| PK-MISSING-SECTION | medium | d | A standard specification section has no heading (MPEP 608.01(a)); usually it also means missing content. |
| PK-NO-FIGURES | medium | d | The disclosure references no figures, where 37 CFR 1.81 requires a drawing if one is necessary to understand the invention. |
| PK-FIGURE-GAP | low | d | Figure numbers are referenced with a gap — either a missing drawing or a stale reference. |
| PK-NUMERAL-CONFLICT | medium | d | One reference numeral is attached to two different element names, making the specification ambiguous. |
| PK-VAGUE-ENABLEMENT | medium | d p c | The disclosure leans on any suitable or known in the art, which is where enablement quietly goes missing. |
| PK-ELIGIBILITY-SIGNAL | medium | d p c | Abstract-idea vocabulary with no technical-improvement language — the shape that draws a 101 rejection. Informational, not a verdict. |
Renaming a flag id breaks the reconciliation contract in both directions: the model has no entry to
return and your assertion has nothing to match. Treat the PK-* ids as an interface.
You may send an empty prescan, or omit it entirely. The lane still runs; it simply has
fewer deterministic facts to ground itself in, coverage_check comes back as an empty
array, and nothing checks the model's counting for you.
1. A tiny client
A few lines of setup that every later step reuses: the base URL, the bearer token, and a JSON post
that raises on the error branch of the envelope. Two headers, no more —
Authorization and Content-Type. The slug constant is here only because
step 2 needs it in a request body; it never becomes a header. Replace the
"YOUR_TOKEN" placeholder by reading the token from wherever your program keeps
secrets rather than committing it.
# Every call in this document uses these three values.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="patent-desk" # used once, in the POST /guest body
TOKEN="YOUR_TOKEN" # from https://patent-desk.skillsafe.ai/tokens.html
post() { # post <path> <json>
curl -sS -X POST "$BASE$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
}
import json
import urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "patent-desk" # used once, in the POST /guest body
TOKEN = "YOUR_TOKEN" # from https://patent-desk.skillsafe.ai/tokens.html
def call(path, payload=None, method="POST", idempotency_key=None):
body = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(BASE + path, data=body, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
if idempotency_key:
req.add_header("Idempotency-Key", idempotency_key)
with urllib.request.urlopen(req) as resp:
envelope = json.loads(resp.read())
if not envelope.get("ok"):
raise RuntimeError(envelope["error"]["code"] + ": " + envelope["error"]["message"])
return envelope["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "patent-desk"; // used once, in the POST /guest body
const TOKEN = "YOUR_TOKEN"; // from https://patent-desk.skillsafe.ai/tokens.html
async function call(path, payload, method = "POST", idempotencyKey) {
const headers = {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
};
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
const res = await fetch(BASE + path, {
method,
headers,
body: payload === undefined ? undefined : JSON.stringify(payload)
});
const envelope = await res.json();
if (!envelope.ok) {
throw new Error(`${envelope.error.code}: ${envelope.error.message}`);
}
return envelope.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const (
base = "https://api.skillsafe.ai/v1/app-api"
slug = "patent-desk" // used once, in the POST /guest body
)
// token reads PATENT_DESK_TOKEN, or falls back to the placeholder.
func token() string {
if t := os.Getenv("PATENT_DESK_TOKEN"); t != "" {
return t
}
return "YOUR_TOKEN" // from https://patent-desk.skillsafe.ai/tokens.html
}
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(method, path string, payload any, idemKey string) (json.RawMessage, error) {
var body io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
body = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, body)
req.Header.Set("Authorization", "Bearer "+token())
req.Header.Set("Content-Type", "application/json")
if idemKey != "" {
req.Header.Set("Idempotency-Key", idemKey)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class PatentDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "patent-desk"; // used once, in the POST /guest body
static final String TOKEN = "YOUR_TOKEN"; // from https://patent-desk.skillsafe.ai/tokens.html
static final HttpClient CLIENT = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> res = CLIENT.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) {
throw new RuntimeException("HTTP " + res.statusCode() + ": " + res.body());
}
return res.body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "patent-desk" # used once, in the POST /guest body
TOKEN = "YOUR_TOKEN" # from https://patent-desk.skillsafe.ai/tokens.html
def call(path, payload = nil, method = :post, idempotency_key: nil)
uri = URI(BASE + path)
req = method == :get ? Net::HTTP::Get.new(uri) : Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = idempotency_key if idempotency_key
req.body = JSON.dump(payload) unless payload.nil?
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
envelope = JSON.parse(res.body)
raise "#{envelope['error']['code']}: #{envelope['error']['message']}" unless envelope["ok"]
envelope["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "patent-desk"; // used once, in the POST /guest body
const TOKEN = "YOUR_TOKEN"; // from https://patent-desk.skillsafe.ai/tokens.html
function call(string $path, ?array $payload = null, ?string $idemKey = null): array {
$headers = [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
];
if ($idemKey !== null) {
$headers[] = "Idempotency-Key: " . $idemKey;
}
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => $payload !== null,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload === null ? "" : json_encode($payload),
]);
$envelope = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($envelope["ok"])) {
throw new RuntimeException($envelope["error"]["code"] . ": " . $envelope["error"]["message"]);
}
return $envelope["data"];
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
public static class PatentDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "patent-desk"; // used once, in the POST /guest body
const string Token = "YOUR_TOKEN"; // from https://patent-desk.skillsafe.ai/tokens.html
static readonly HttpClient Client = new HttpClient();
public static async Task<JsonElement> CallAsync(
string path, object payload = null, string idemKey = null, HttpMethod method = null)
{
var req = new HttpRequestMessage(method ?? HttpMethod.Post, Base + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (idemKey is not null) req.Headers.Add("Idempotency-Key", idemKey);
if (payload is not null)
{
req.Content = new StringContent(
JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
}
var res = await Client.SendAsync(req);
var envelope = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!envelope.GetProperty("ok").GetBoolean())
{
var err = envelope.GetProperty("error");
throw new Exception($"{err.GetProperty("code")}: {err.GetProperty("message")}");
}
return envelope.GetProperty("data");
}
}
2. Get a token
A guest token is minted on demand and is enough for /me and
/estimate. Running a lane is metered, so it needs a
personal token — sign in at the token page and copy
it from there. This is the one and only call that names the app: {"slug":
"patent-desk"} in the JSON body. Every POST /guest mints a
new guest identity, so reuse one token across a session rather than minting per request.
curl -sS -X POST "$BASE/guest" \
-H "Content-Type: application/json" \
-d "{\"slug\":\"$SLUG\"}" | tee guest.json
# {"ok":true,"data":{"token":"aut_...","subject_type":"guest"}}
TOKEN=$(python3 -c "import json;print(json.load(open('guest.json'))['data']['token'])")
import json, urllib.request
body = json.dumps({"slug": SLUG}).encode()
req = urllib.request.Request(BASE + "/guest", data=body, method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as resp:
guest = json.loads(resp.read())["data"]
TOKEN = guest["token"] # reuse this for the whole session
print(guest["subject_type"]) # "guest"
const res = await fetch(`${BASE}/guest`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: SLUG })
});
const guest = (await res.json()).data;
const token = guest.token; // reuse for the whole session
console.log(guest.subject_type); // "guest"
guestBody := bytes.NewReader([]byte(`{"slug":"patent-desk"}`))
req, _ := http.NewRequest("POST", base+"/guest", guestBody)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var env struct {
Data struct {
Token string `json:"token"`
SubjectType string `json:"subject_type"`
} `json:"data"`
}
json.NewDecoder(res.Body).Decode(&env)
fmt.Println(env.Data.SubjectType) // "guest"
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"patent-desk\"}"))
.build();
HttpResponse<String> res = CLIENT.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
// {"ok":true,"data":{"token":"aut_...","subject_type":"guest"}}
uri = URI(BASE + "/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.dump({ "slug" => SLUG })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
guest = JSON.parse(res.body)["data"]
token = guest["token"] # reuse for the whole session
puts guest["subject_type"] # "guest"
<?php
$ch = curl_init(BASE . "/guest");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["slug" => SLUG]),
]);
$guest = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
$token = $guest["token"]; // reuse for the whole session
echo $guest["subject_type"]; // "guest"
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/guest");
req.Content = new StringContent("{\"slug\":\"patent-desk\"}", Encoding.UTF8, "application/json");
var res = await Client.SendAsync(req);
var guest = JsonDocument.Parse(await res.Content.ReadAsStringAsync())
.RootElement.GetProperty("data");
var token = guest.GetProperty("token").GetString(); // reuse for the session
Console.WriteLine(guest.GetProperty("subject_type")); // "guest"
3. Check who you are and what you can spend
GET /me is free and tells you the subject type and the credit balance. Compare that
balance against the estimate in the next step before you run anything — a 402 after submitting
a disclosure is a failure of the client, not of the user.
curl -sS "$BASE/me" \
-H "Authorization: Bearer $TOKEN"
# {"ok":true,"data":{"subject_type":"user","credits":48210, ...}}
me = call("/me", method="GET")
print(me["subject_type"], me["credits"])
const me = await call("/me", undefined, "GET");
console.log(me.subject_type, me.credits);
data, err := call("GET", "/me", nil, "")
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
json.Unmarshal(data, &me)
fmt.Println(me.SubjectType, me.Credits)
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/me"))
.header("Authorization", "Bearer " + TOKEN)
.GET()
.build();
System.out.println(CLIENT.send(req, HttpResponse.BodyHandlers.ofString()).body());
me = call("/me", nil, :get)
puts "#{me['subject_type']} #{me['credits']}"
<?php
$ch = curl_init(BASE . "/me");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN],
]);
$me = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
echo $me["subject_type"] . " " . $me["credits"];
var me = await CallAsync("/me", method: HttpMethod.Get);
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
4. Price the run before you make it
POST /estimate is free, creates no job and charges nothing. It takes the same body the
run will take — the input object itself, not wrapped in an input
key — and returns the model, the markup and the two numbers that matter.
hold_credits is the amount reserved against your balance, priced at the full output
cap; the actual charge is usually far lower, so present it as reserved and never as the price.
min_credits is the floor you must be able to cover for the run to be accepted at all.
The hold differs per lane. The four lanes have different output caps — a
claims or review112 run that has to reproduce a whole claim set reserves
considerably more than a disclosure read. Estimate the lane you are about to run, and
re-estimate whenever task changes.
post /estimate '{
"task": "review112",
"disclosure": "TECHNICAL FIELD\n\nThis disclosure relates to safety monitoring of ...",
"claims_text": "1. A method for detecting incipient thermal runaway ...",
"jurisdiction": "US"
}'
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":5140,"min_credits":460}}
payload = {
"task": "review112",
"disclosure": open("disclosure.txt").read(),
"claims_text": open("claims.txt").read(),
"jurisdiction": "US",
}
est = call("/estimate", payload)
print(est["model"], est["hold_credits"], est["min_credits"])
if me["credits"] < est["min_credits"]:
raise SystemExit(f"short by {est['min_credits'] - me['credits']} credits")
import { readFileSync } from "node:fs";
const payload = {
task: "review112",
disclosure: readFileSync("disclosure.txt", "utf8"),
claims_text: readFileSync("claims.txt", "utf8"),
jurisdiction: "US"
};
const est = await call("/estimate", payload);
console.log(est.model, est.hold_credits, est.min_credits);
if (me.credits < est.min_credits) {
throw new Error(`short by ${est.min_credits - me.credits} credits`);
}
disclosure, _ := os.ReadFile("disclosure.txt")
claims, _ := os.ReadFile("claims.txt")
payload := map[string]any{
"task": "review112",
"disclosure": string(disclosure),
"claims_text": string(claims),
"jurisdiction": "US",
}
data, err := call("POST", "/estimate", payload, "")
if err != nil {
panic(err)
}
var est struct {
Model string `json:"model"`
HoldCredits int `json:"hold_credits"`
MinCredits int `json:"min_credits"`
}
json.Unmarshal(data, &est)
fmt.Println(est.Model, est.HoldCredits, est.MinCredits)
String disclosure = Files.readString(Path.of("disclosure.txt"));
String claims = Files.readString(Path.of("claims.txt"));
// The body is the input object itself - there is no "input" wrapper.
String payload = """
{"task": "review112", "disclosure": %s, "claims_text": %s, "jurisdiction": "US"}
""".formatted(JsonUtil.quote(disclosure), JsonUtil.quote(claims));
String estimate = call("/estimate", payload);
System.out.println(estimate);
// {"ok":true,"data":{"model":"gpt-5.6-terra","hold_credits":5140, ...}}
payload = {
"task" => "review112",
"disclosure" => File.read("disclosure.txt"),
"claims_text" => File.read("claims.txt"),
"jurisdiction" => "US"
}
est = call("/estimate", payload)
puts "#{est['model']} #{est['hold_credits']} #{est['min_credits']}"
abort "short by #{est['min_credits'] - me['credits']}" if me["credits"] < est["min_credits"]
<?php
$payload = [
"task" => "review112",
"disclosure" => file_get_contents("disclosure.txt"),
"claims_text" => file_get_contents("claims.txt"),
"jurisdiction" => "US",
];
$est = call("/estimate", $payload);
echo "{$est['model']} {$est['hold_credits']} {$est['min_credits']}\n";
if ($me["credits"] < $est["min_credits"]) {
throw new RuntimeException("short by " . ($est["min_credits"] - $me["credits"]));
}
var payload = new
{
task = "review112",
disclosure = await File.ReadAllTextAsync("disclosure.txt"),
claims_text = await File.ReadAllTextAsync("claims.txt"),
jurisdiction = "US"
};
var est = await CallAsync("/estimate", payload);
Console.WriteLine($"{est.GetProperty("model")} {est.GetProperty("hold_credits")}");
5. Run a lane and poll for the result
POST /run takes the input object as its body and returns a job_id
immediately; poll GET /jobs/{job_id} until status is terminal
(succeeded, failed or cancelled). The model's reply is a
string at data.output.output, so parse it yourself.
Always send an Idempotency-Key, and derive it from
(task, input, attempt). All three parts matter. Include task or the second
lane over the same disclosure will collide with the first and hand you back the first lane's cached
result. Include a hash of the input so editing a claim starts a new run. Include an attempt counter so
a deliberate re-ask (a retry_note re-ask, say) is a new run while a transport-level
retry of the same attempt reuses the key and cannot double-bill.
ATTEMPT=1
HASH=$(cat disclosure.txt claims.txt | shasum -a 256 | cut -c1-16)
KEY="patent-desk:review112:$HASH:$ATTEMPT"
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d @payload.json | python3 -c "import json,sys;print(json.load(sys.stdin)['data']['job_id'])")
until curl -sS "$BASE/jobs/$JOB" -H "Authorization: Bearer $TOKEN" \
| tee job.json | grep -q '"status":"succeeded"'; do sleep 2; done
python3 -c "import json;print(json.load(open('job.json'))['data']['output']['output'])"
import hashlib, time
def idem_key(payload, attempt=1):
material = payload["disclosure"] + "\x00" + payload.get("claims_text", "")
digest = hashlib.sha256(material.encode()).hexdigest()[:16]
return f"patent-desk:{payload['task']}:{digest}:{attempt}"
job = call("/run", payload, idempotency_key=idem_key(payload))
job_id = job["job_id"]
while True:
status = call("/jobs/" + job_id, method="GET")
if status["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
if status["status"] != "succeeded":
raise RuntimeError("job " + status["status"])
result = json.loads(status["output"]["output"])
print(result["posture"], "-", result["verdict"])
for f in result["findings"]:
print(f" [{f['severity']:8}] {f['id']} claim {f['claim_no']}: {f['title']}")
import { createHash } from "node:crypto";
function idemKey(payload, attempt = 1) {
const material = `${payload.disclosure}\u0000${payload.claims_text ?? ""}`;
const digest = createHash("sha256").update(material).digest("hex").slice(0, 16);
return `patent-desk:${payload.task}:${digest}:${attempt}`;
}
const job = await call("/run", payload, "POST", idemKey(payload));
let status;
do {
await new Promise(r => setTimeout(r, 2000));
status = await call(`/jobs/${job.job_id}`, undefined, "GET");
} while (!["succeeded", "failed", "cancelled"].includes(status.status));
if (status.status !== "succeeded") throw new Error(`job ${status.status}`);
const result = JSON.parse(status.output.output);
console.log(result.posture, "-", result.verdict);
for (const f of result.findings) {
console.log(` [${f.severity}] ${f.id} claim ${f.claim_no}: ${f.title}`);
}
material := payload["disclosure"].(string) + "\x00" + payload["claims_text"].(string)
sum := sha256.Sum256([]byte(material))
key := fmt.Sprintf("patent-desk:%s:%x:1", payload["task"], sum[:8])
data, err := call("POST", "/run", payload, key)
if err != nil {
panic(err)
}
var job struct {
JobID string `json:"job_id"`
}
json.Unmarshal(data, &job)
for {
time.Sleep(2 * time.Second)
statusData, _ := call("GET", "/jobs/"+job.JobID, nil, "")
var st struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
json.Unmarshal(statusData, &st)
if st.Status == "succeeded" {
fmt.Println(st.Output.Output)
break
}
if st.Status == "failed" || st.Status == "cancelled" {
panic("job " + st.Status)
}
}
String material = disclosure + "\u0000" + claims;
String digest = Integer.toHexString(material.hashCode());
String key = "patent-desk:review112:" + digest + ":1";
HttpRequest run = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
String jobId = JsonUtil.path(
CLIENT.send(run, HttpResponse.BodyHandlers.ofString()).body(), "data", "job_id");
String status;
do {
Thread.sleep(2000);
status = call("/jobs/" + jobId, "");
} while (!status.contains("\"status\":\"succeeded\"")
&& !status.contains("\"status\":\"failed\""));
System.out.println(status);
require "digest"
def idem_key(payload, attempt = 1)
material = "#{payload['disclosure']}\0#{payload['claims_text']}"
"patent-desk:#{payload['task']}:#{Digest::SHA256.hexdigest(material)[0, 16]}:#{attempt}"
end
job = call("/run", payload, :post, idempotency_key: idem_key(payload))
status = nil
loop do
sleep 2
status = call("/jobs/#{job['job_id']}", nil, :get)
break if %w[succeeded failed cancelled].include?(status["status"])
end
raise "job #{status['status']}" unless status["status"] == "succeeded"
result = JSON.parse(status["output"]["output"])
puts "#{result['posture']} - #{result['verdict']}"
result["findings"].each { |f| puts " [#{f['severity']}] #{f['id']} #{f['title']}" }
<?php
function idem_key(array $payload, int $attempt = 1): string {
$material = $payload["disclosure"] . "\0" . ($payload["claims_text"] ?? "");
$digest = substr(hash("sha256", $material), 0, 16);
return "patent-desk:{$payload['task']}:{$digest}:{$attempt}";
}
$job = call("/run", $payload, idem_key($payload));
do {
sleep(2);
$status = call("/jobs/" . $job["job_id"]);
} while (!in_array($status["status"], ["succeeded", "failed", "cancelled"], true));
if ($status["status"] !== "succeeded") {
throw new RuntimeException("job " . $status["status"]);
}
$result = json_decode($status["output"]["output"], true);
echo "{$result['posture']} - {$result['verdict']}\n";
foreach ($result["findings"] as $f) {
echo " [{$f['severity']}] {$f['id']} {$f['title']}\n";
}
using System.Security.Cryptography;
var material = payload.disclosure + "\0" + payload.claims_text;
var digest = Convert.ToHexString(
SHA256.HashData(Encoding.UTF8.GetBytes(material)))[..16].ToLower();
var key = $"patent-desk:{payload.task}:{digest}:1";
var job = await CallAsync("/run", payload, key);
var jobId = job.GetProperty("job_id").GetString();
JsonElement status;
string state;
do
{
await Task.Delay(2000);
status = await CallAsync($"/jobs/{jobId}", method: HttpMethod.Get);
state = status.GetProperty("status").GetString();
} while (state is not ("succeeded" or "failed" or "cancelled"));
if (state != "succeeded") throw new Exception($"job {state}");
var result = JsonDocument.Parse(
status.GetProperty("output").GetProperty("output").GetString()).RootElement;
Console.WriteLine(result.GetProperty("verdict"));
6. Stream it instead, for anything interactive
POST /run-stream is the same call with the same body over Server-Sent Events. Deltas
arrive as they are generated, which is what the page uses to advance its progress stages while a
claim set is being rewritten. The same Idempotency-Key rule applies. Accumulate the
deltas and parse the JSON once the stream closes — a half-received envelope is not valid JSON
and a claim rewritten halfway is worse than no claim.
curl -sS -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d @payload.json
# event: delta
# data: {"text":"{\"lane\":\"review112\","}
# event: done
# data: {"job_id":"job_...","charged_credits":3187,"truncated":false}
import urllib.request
req = urllib.request.Request(BASE + "/run-stream", data=json.dumps(payload).encode())
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", idem_key(payload))
req.add_header("Accept", "text/event-stream")
chunks = []
with urllib.request.urlopen(req) as stream:
for raw in stream:
line = raw.decode().strip()
if not line.startswith("data:"):
continue
event = json.loads(line[5:].strip())
if "text" in event:
chunks.append(event["text"])
print(".", end="", flush=True)
result = json.loads("".join(chunks))
print("\n", result["posture"], result["verdict"])
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": idemKey(payload),
"Accept": "text/event-stream"
},
body: JSON.stringify(payload)
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "", text = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const event = JSON.parse(line.slice(5).trim());
if (event.text) text += event.text;
}
}
const result = JSON.parse(text);
console.log(result.posture, result.verdict);
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token())
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
req.Header.Set("Accept", "text/event-stream")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var sb strings.Builder
scanner := bufio.NewScanner(res.Body)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "data:") {
continue
}
var ev struct {
Text string `json:"text"`
}
if json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &ev) == nil && ev.Text != "" {
sb.WriteString(ev.Text)
}
}
fmt.Println(sb.String())
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
StringBuilder text = new StringBuilder();
CLIENT.send(req, HttpResponse.BodyHandlers.ofLines())
.body()
.filter(line -> line.startsWith("data:"))
.forEach(line -> text.append(JsonUtil.path(line.substring(5).trim(), "text")));
System.out.println(text);
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = idem_key(payload)
req["Accept"] = "text/event-stream"
req.body = JSON.dump(payload)
text = +""
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|
next unless line.start_with?("data:")
event = JSON.parse(line[5..].strip) rescue next
text << event["text"] if event["text"]
end
end
end
end
result = JSON.parse(text)
puts "#{result['posture']} #{result['verdict']}"
<?php
$text = "";
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Idempotency-Key: " . idem_key($payload),
"Accept: text/event-stream",
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$text) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "data:")) {
$event = json_decode(trim(substr($line, 5)), true);
if (isset($event["text"])) $text .= $event["text"];
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
$result = json_decode($text, true);
echo "{$result['posture']} {$result['verdict']}\n";
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.Add("Idempotency-Key", key);
req.Headers.Add("Accept", "text/event-stream");
req.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var res = await Client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var text = new StringBuilder();
while (await reader.ReadLineAsync() is { } line)
{
if (!line.StartsWith("data:")) continue;
var ev = JsonDocument.Parse(line[5..].Trim()).RootElement;
if (ev.TryGetProperty("text", out var t)) text.Append(t.GetString());
}
var result = JsonDocument.Parse(text.ToString()).RootElement;
Console.WriteLine(result.GetProperty("verdict"));
7. One worked example per lane
All four requests below send the same disclosure — an acoustic-emission detector
for incipient thermal runaway in a lithium-ion pack, the material the app ships as its free example
— and differ in task, in whether they send claims_text, and in the
lane-filtered prescan. The envelope is identical across all four; only
body and artifact change. Arrays shown as ... follow the
shapes documented above, abbreviated here to one representative entry.
task: "disclosure" — Read the intake
What the invention is, the elements it needs and how well each is described, the novelty candidates, the statutory posture, where 112(a) support is thin, and what to ask the inventor before anyone drafts. This is the only lane that emits no file.
Request
{
"task": "disclosure",
"disclosure": "<the invention disclosure>",
"jurisdiction": "US",
"context": "We think the new part is the current-steadiness gate, not the acoustic sensing itself.",
"prescan": {
"resources": [
{"id": "PR-DISCLOSURE-WORDS", "label": "612 words of disclosure"},
{"id": "PR-FIGURES", "label": "figures referenced: FIG. 1, FIG. 2, FIG. 4"}
],
"flags": [
{"id": "PK-NUMERAL-CONFLICT", "severity": "medium",
"label": "one reference numeral is attached to two different element names",
"claim_no": 0, "claims": [], "line": 41, "occurrences": 1,
"terms": ["104"], "detail": "104: acoustic sensor, coolant pump"},
{"id": "PK-VAGUE-ENABLEMENT", "severity": "medium",
"label": "the disclosure leans on 'any suitable' or 'known in the art'",
"claim_no": 0, "claims": [], "line": 40, "occurrences": 2,
"terms": ["any suitable", "conventional means"], "detail": ""},
{"id": "PK-FIGURE-GAP", "severity": "low",
"label": "figure numbers are referenced with a gap",
"claim_no": 0, "claims": [], "line": 33, "occurrences": 1,
"terms": [], "detail": "FIG. 2 to FIG. 4"}
]
}
}
Response data.output.output, parsed
{
"lane": "disclosure",
"lane_inferred": false,
"invention": "acoustic-emission detection of incipient thermal runaway in a battery pack",
"title": "Acoustic runaway detection - intake read",
"posture": "needs-work",
"verdict": "The invention is identifiable and arguably novel, but one numeral names two parts and the adhesive and filter design are left to \"any suitable\" means.",
"statutory_classes": ["method", "apparatus"],
"summary": "The disclosure describes a housing-mounted piezoelectric sensor whose 150-400 kHz burst rate is gated on steady pack current ...",
"assumptions": ["US practice applied; no other jurisdiction was named."],
"open_questions": ["What is the measured false-positive rate without the current-steadiness gate?"],
"findings": [
{"id": "PD-001", "title": "Give reference numeral 104 to one element only",
"severity": "high", "area": "formalities", "claim_no": 0, "term": "104",
"evidence": "An acoustic sensor 104 is bonded to the inner face of the pack housing 102",
"why": "A numeral must name one element throughout; 37 CFR 1.84(p) and MPEP 608.01. Reusing 104 for the coolant pump makes every claim that leans on it ambiguous.",
"fix": "Renumber the coolant pump, e.g. to 106, everywhere it appears.",
"fix_text": ""}
],
"coverage_check": [
{"flag_id": "PK-NUMERAL-CONFLICT", "status": "confirmed", "finding_id": "PD-001", "note": ""},
{"flag_id": "PK-VAGUE-ENABLEMENT", "status": "confirmed", "finding_id": "PD-002",
"note": "Both sites need a worked example; the adhesive one is the weaker."},
{"flag_id": "PK-FIGURE-GAP", "status": "confirmed", "finding_id": "PD-003", "note": ""}
],
"artifact": {"kind": "none", "filename": "", "content": ""},
"next_lane": {"lane": "priorart", "reason": "Acoustic emission monitoring of cells is a known field; the search must run before claim scope is chosen."},
"body": {
"inventive_concept": "Gating an acoustic burst-rate threshold on the pack current being steady, so gassing is distinguished from mechanical creak under a load step.",
"problem_solved": "Surface thermistors detect runaway roughly ninety seconds too late to do anything but vent.",
"elements": [
{"name": "acoustic sensor", "role": "Hears 150-400 kHz emission bursts from every cell in the module.",
"essential": true, "disclosed": "full",
"evidence": "a piezoelectric element with a usable response from 100 kHz to 500 kHz"},
{"name": "bonding adhesive", "role": "Couples the sensor to the housing.",
"essential": true, "disclosed": "absent",
"evidence": "bonded to the inner face of the pack housing 102 with any suitable adhesive"}
],
"novel_features": [
{"feature": "current-steadiness gate on the burst-rate threshold",
"why_novel": "The applicant identifies this, not the acoustic sensing, as the new part.",
"evidence": "while the pack current, measured by a shunt 110, has varied by less than two percent",
"confidence": "medium"}
],
"statutory_posture": {
"classes": ["method", "apparatus"],
"eligibility_risk": "low",
"reason": "The claim reads on a physical sensor and a contactor actuation, not on a calculation alone.",
"what_would_lower_it": "Keep the contactor-opening step in every independent claim."
},
"support_gaps": [
{"requirement": "enablement", "gap": "No adhesive is named and no bond-line requirement is given.",
"why_it_matters": "35 U.S.C. 112(a): acoustic coupling through the bond is what makes the sensor work at all.",
"what_to_add": "One worked adhesive, its thickness and the coupling loss it gives."}
],
"questions_for_inventor": ["Was the 150-400 kHz band chosen empirically, and over how many cell chemistries?"],
"readiness": {"score_of_10": 6, "blockers": ["The adhesive and band-pass design are not enabled."], "ready_to_draft": false}
}
}
task: "priorart" — Build the search
Classifications, a concept matrix, at least four queries each different in kind, a five-to-eight-step
protocol ending in a step that records what was found, and the criteria by which a hit would knock
out a claim element. This lane produces the search, never its results — the
model has no search access on the run and will not name a patent number, a publication or a date as
prior art. artifact.kind is markdown: a search log to fill in while
working.
Request
{
"task": "priorart",
"disclosure": "<the invention disclosure>",
"jurisdiction": "US",
"context": "Competitor packs use surface thermistors only. Nothing published or sold yet.",
"prescan": {
"resources": [{"id": "PR-TECHNICAL", "label": "technical-effect vocabulary present: threshold, filter, sensor, band-pass"}],
"flags": [
{"id": "PK-VAGUE-ENABLEMENT", "severity": "medium",
"label": "the disclosure leans on 'any suitable' or 'known in the art'",
"claim_no": 0, "claims": [], "line": 40, "occurrences": 2,
"terms": ["any suitable", "conventional means"], "detail": ""}
]
}
}
Response data.output.output, parsed
{
"lane": "priorart",
"lane_inferred": false,
"invention": "acoustic-emission detection of incipient thermal runaway in a battery pack",
"title": "Acoustic runaway detection - search plan",
"posture": "filing-ready",
"verdict": "The technology classifies cleanly into battery-management and acoustic-emission testing, so the search is executable as written.",
"statutory_classes": ["method", "apparatus"],
"summary": "Four queries in two schemes, aimed at the intersection of acoustic emission sensing and battery safety cut-off ...",
"assumptions": ["US practice applied."],
"open_questions": ["Is there an internal disclosure date that fixes the cut-off for this search?"],
"findings": [
{"id": "PD-001", "title": "Search the current-steadiness gate separately",
"severity": "high", "area": "search", "claim_no": 0, "term": "current-steadiness gate",
"evidence": "the burst rate exceeds forty bursts per second while the pack current ... has varied by less than two percent",
"why": "This is the applicant's identified point of novelty, and a single reference showing it with acoustic sensing would anticipate claim 1 under 102.",
"fix": "Run Q3 before any drafting decision on claim 1's breadth.", "fix_text": ""}
],
"coverage_check": [
{"flag_id": "PK-VAGUE-ENABLEMENT", "status": "set-aside",
"finding_id": "", "note": "Real, but an enablement matter the disclosure lane owns; it does not change what to search."}
],
"artifact": {"kind": "markdown", "filename": "search-log.md",
"content": "# Prior-art search log\n\n| Query | Engine | Date run | Hits | Kept | Why kept |\n|---|---|---|---|---|---|\n| Q1 | ... |\n"},
"next_lane": {"lane": "claims", "reason": "Claim scope should be chosen after Q1-Q4 have been run."},
"body": {
"search_scope": {
"technology": "acoustic emission monitoring of electrochemical cells for safety cut-off",
"problem": "detecting internal gas generation before casing temperature rises",
"solution": "band-limited burst counting gated on steady pack current",
"field_of_search": "In scope: battery management, acoustic emission NDT, cell venting prediction. Out of scope: ultrasonic state-of-charge estimation by transit time."
},
"classifications": [
{"scheme": "CPC", "code": "H01M 10/48",
"definition": "Accumulators combined with arrangements for measuring, testing or indicating condition.",
"why": "The monitoring system is combined with the pack itself.", "confidence": "high"},
{"scheme": "CPC", "code": "G01N 29/14",
"definition": "Investigating materials by detecting acoustic emission arising from the material itself.",
"why": "Bursts originate in the cell, not from an injected pulse.", "confidence": "high"},
{"scheme": "IPC", "code": "",
"definition": "Battery thermal-runaway prediction by non-thermal sensing.",
"why": "Described in words rather than by code: no IPC subclass is confidently known for the intersection.",
"confidence": "low"}
],
"concept_matrix": [
{"concept": "acoustic emission", "synonyms": ["ultrasonic emission", "acoustic signature", "AE burst"],
"narrower": ["piezoelectric burst count"], "broader": ["vibration sensing"], "must_appear": true},
{"concept": "thermal runaway", "synonyms": ["cell venting", "exothermic excursion"],
"narrower": ["incipient runaway"], "broader": ["battery fault"], "must_appear": true}
],
"queries": [
{"id": "Q1", "engine": "Google Patents",
"query": "CPC=(H01M10/48) AND CPC=(G01N29/14)",
"targets": "The classification intersection - anything already sitting in both subclasses.",
"expected_noise": "Ultrasonic state-of-charge measurement, which is a transit-time method."},
{"id": "Q3", "engine": "Espacenet",
"query": "txt=(acoustic OR ultrasonic) AND txt=(\"burst rate\" OR \"emission rate\") AND txt=(current AND (steady OR stable OR quiescent))",
"targets": "The current-steadiness gate specifically, which is the applicant's point of novelty.",
"expected_noise": "Charger-side ripple measurement, which uses the same vocabulary."}
],
"protocol": [
{"step": 1, "name": "Classification sweep", "action": "Run Q1 and read titles and first claims only.",
"stop_condition": "Every hit is triaged keep or discard, with a one-line reason."},
{"step": 6, "name": "Record the search", "action": "Fill the search log with engine, date, hit count, references kept and why.",
"stop_condition": "A colleague could re-run the search from the log alone."}
],
"non_patent_sources": ["Journal of Power Sources, acoustic characterisation of Li-ion failure", "SAE J2464 abuse-testing procedure"],
"knockout_criteria": [
{"claim_element": "gating a burst-rate threshold on pack-current steadiness",
"anticipated_if": "one reference shows acoustic burst counting, a threshold comparison and a current-steadiness condition together",
"obvious_if": "one reference shows acoustic burst counting with a threshold and another shows current-steadiness gating of any battery fault signal, with a stated reason to combine",
"fallback_position": "the adaptive threshold referenced to the quietest window since rest"}
],
"limits": [
"This plan was produced without running any search: no result here is a search result.",
"Unpublished applications, up to eighteen months old, cannot be found by any of these queries.",
"Non-English filings will be reached only through the classification queries."
]
}
}
task: "claims" — Draft the set
The broadest claim the pasted material actually enables and describes, plus the ladder of narrower
positions to retreat to: one broad independent claim in the primary class, dependents adding one
limitation each, and a second or third independent claim in the other classes the disclosure supports.
Send claims_text and it becomes a draft to revise instead, keeping your numbering where
possible and explaining each change in that claim's rationale.
artifact.content is the set as plain numbered text — the string the review lane
consumes.
Request
{
"task": "claims",
"disclosure": "<the invention disclosure>",
"jurisdiction": "US",
"context": "Audience is our outside counsel, who will draft the real application.",
"prescan": {
"resources": [{"id": "PR-DISCLOSURE-WORDS", "label": "612 words of disclosure"}],
"flags": [
{"id": "PK-VAGUE-ENABLEMENT", "severity": "medium",
"label": "the disclosure leans on 'any suitable' or 'known in the art'",
"claim_no": 0, "claims": [], "line": 40, "occurrences": 2,
"terms": ["any suitable", "conventional means"], "detail": ""}
]
}
}
Response data.output.output, parsed
{
"lane": "claims",
"lane_inferred": false,
"invention": "acoustic-emission detection of incipient thermal runaway in a battery pack",
"title": "Acoustic runaway detection - claim set",
"posture": "needs-work",
"verdict": "A three-class set is supportable, but the adhesive is not enabled well enough to appear in any claim.",
"statutory_classes": ["method", "apparatus", "crm"],
"summary": "Claim 1 is the method: receive, band-pass, count in a sliding window, and open the contactor on a burst-rate threshold with the current steady ...",
"assumptions": ["Three independent claims and twelve total, so no excess-claim fee applies."],
"open_questions": ["Should the 150-400 kHz band appear in claim 1 or only in a dependent?"],
"findings": [
{"id": "PD-001", "title": "Leave the adhesive out of every claim",
"severity": "medium", "area": "support", "claim_no": 0, "term": "any suitable adhesive",
"evidence": "bonded to the inner face of the pack housing 102 with any suitable adhesive",
"why": "35 U.S.C. 112(a): a limitation whose only description is \"any suitable\" is unsupported, and it would be the easiest term in the set to attack.",
"fix": "Claim the coupling functionally - the sensor being acoustically coupled to the housing - and list the adhesive in not_claimed.",
"fix_text": "an acoustic sensor acoustically coupled to a housing of the battery pack"}
],
"coverage_check": [
{"flag_id": "PK-VAGUE-ENABLEMENT", "status": "confirmed", "finding_id": "PD-001",
"note": "Both occurrences bear on claim scope; the adhesive one is the one that changes the claim."}
],
"artifact": {
"kind": "claims",
"filename": "claims.txt",
"content": "1. A method for detecting incipient thermal runaway in a battery pack, the method comprising: receiving an acoustic signal from an acoustic sensor acoustically coupled to a housing of the battery pack; ...\n\n2. The method of claim 1, wherein the sliding window is ten seconds.\n\n3. ...\n"
},
"next_lane": {"lane": "review112", "reason": "Run the drafted set back through the 112 review before it goes to counsel."},
"body": {
"strategy": {
"primary_class": "method",
"coverage_theory": "The method claim reads on the pack maker operating the vehicle pack; the apparatus claim reads on the module supplier shipping the sensor and controller; the CRM claim reads on a firmware vendor shipping the detection code alone.",
"breadth_ladder": "Claim 1 gives up the specific band and the specific threshold; claims 2-5 win back the ten-second window, the forty-burst threshold, the 150-400 kHz band and the adaptive threshold."
},
"claim_set": [
{"num": 1, "type": "independent", "statutory_class": "method", "depends_on": 0,
"text": "A method for detecting incipient thermal runaway in a battery pack, the method comprising: receiving an acoustic signal from an acoustic sensor acoustically coupled to a housing of the battery pack; band-pass filtering the acoustic signal to produce a filtered signal; counting, in a sliding window, threshold crossings of the filtered signal to produce a burst rate; and opening a contactor of the battery pack when the burst rate exceeds a burst-rate threshold and a pack current varies by less than a predetermined fraction over the sliding window.",
"rationale": "The broadest form the disclosure supports; the current-steadiness condition is recited because it is the identified point of novelty.",
"support": "when the burst rate exceeds forty bursts per second while the pack current ... has varied by less than two percent over the same window"},
{"num": 2, "type": "dependent", "statutory_class": "method", "depends_on": 1,
"text": "The method of claim 1, wherein the sliding window is ten seconds.",
"rationale": "First fallback: the one window length actually bench-tested.",
"support": "maintains a sliding ten-second window of the burst count"}
],
"elements_mapped": [
{"element": "acoustic sensor", "claim_nos": [1, 6, 9],
"disclosure_support": "An acoustic sensor 104 is bonded to the inner face of the pack housing 102"},
{"element": "current shunt", "claim_nos": [1, 7],
"disclosure_support": "while the pack current, measured by a shunt 110, has varied by less than two percent"}
],
"fallbacks": [
{"if_rejected": "claim 1 over acoustic emission monitoring with a fixed threshold",
"narrow_to": "the adaptive threshold referenced to the quietest window since the pack was last at rest",
"from_claim": 5}
],
"counts": {"independent": 3, "dependent": 9, "total": 12},
"not_claimed": ["The bonding adhesive: described only as \"any suitable\", so claiming it would invite a 112(a) rejection."]
}
}
task: "review112" — Review against 35 U.S.C. 112
The lane the app is named for. Claim by claim in numerical order: is every term definite under 112(b),
does every definite reference have antecedent basis, is the claim one sentence in one statutory class,
does a dependent claim further limit its parent as 112(d) requires, is every claimed term supported
under 112(a). Defective claims are rewritten, not merely objected to.
claims_text is required: without it the lane returns
posture: "blocked" and empty body arrays rather than guessing at a claim set.
Request
{
"task": "review112",
"disclosure": "<the invention disclosure>",
"claims_text": "1. A method for detecting incipient thermal runaway in a battery pack, the method comprising: ...",
"jurisdiction": "US",
"prescan": {
"resources": [
{"id": "PR-CLAIM-COUNTS", "label": "10 claims (3 independent, 7 dependent), max chain depth 2"},
{"id": "PR-CLASSES", "label": "statutory classes claimed: method, apparatus, crm"}
],
"flags": [
{"id": "PK-FORWARD-DEP", "severity": "critical",
"label": "a claim depends on a later-numbered claim, or on itself",
"claim_no": 4, "claims": [4], "line": 14, "occurrences": 1, "terms": [], "detail": "4 -> 5"},
{"id": "PK-ANTECEDENT-BASIS", "severity": "high",
"label": "definite reference with no earlier indefinite antecedent",
"claim_no": 6, "claims": [6], "line": 20, "occurrences": 1,
"terms": ["the shunt"], "detail": ""},
{"id": "PK-MIXED-CLASS", "severity": "high",
"label": "one claim mixes statutory classes - method steps and apparatus structure",
"claim_no": 9, "claims": [9], "line": 27, "occurrences": 1,
"terms": ["further comprising the steps of"], "detail": ""},
{"id": "PK-THIN-DEPENDENT", "severity": "medium",
"label": "dependent claim adds almost nothing beyond its reference",
"claim_no": 8, "claims": [8], "line": 25, "occurrences": 1, "terms": [], "detail": ""},
{"id": "PK-RELATIVE-TERM", "severity": "medium",
"label": "relative or approximating term of degree",
"claim_no": 1, "claims": [1], "line": 3, "occurrences": 1,
"terms": ["substantially"], "detail": ""}
]
}
}
Response data.output.output, parsed
{
"lane": "review112",
"lane_inferred": false,
"invention": "acoustic-emission detection of incipient thermal runaway in a battery pack",
"title": "Acoustic runaway detection - 112 review",
"posture": "needs-work",
"verdict": "Claim 4 depends forward on claim 5, which is fatal as filed, and claim 9 mixes an apparatus with method steps.",
"statutory_classes": ["method", "apparatus", "crm"],
"summary": "Ten claims reviewed. Two are unfixable as written without amendment - the forward dependency in claim 4 and the mixed class in claim 9 ...",
"assumptions": ["US practice applied; jurisdiction was US."],
"open_questions": ["Is \"substantially steady\" in claim 1 meant to be the two percent given in the specification?"],
"findings": [
{"id": "PD-001", "title": "Redirect claim 4 to claim 1",
"severity": "critical", "area": "dependency", "claim_no": 4, "term": "claim 5",
"evidence": "4. The method of claim 5, wherein the acoustic sensor is a piezoelectric element.",
"why": "37 CFR 1.75(c) requires a dependent claim to refer to a preceding claim; a forward reference is objected to on sight.",
"fix": "Depend claim 4 on claim 1, or renumber so the referenced claim precedes it.",
"fix_text": "The method of claim 1, wherein the acoustic sensor is a piezoelectric element."},
{"id": "PD-002", "title": "Give \"the shunt\" an antecedent",
"severity": "high", "area": "antecedent-basis", "claim_no": 6, "term": "the shunt",
"evidence": "6. The method of claim 1, wherein the pack current is measured by the shunt.",
"why": "35 U.S.C. 112(b): no earlier claim introduces \"a shunt\", so the definite reference has no antecedent basis.",
"fix": "Introduce it indefinitely in this claim.",
"fix_text": "The method of claim 1, wherein the pack current is measured by a shunt coupled in series with the battery pack."}
],
"coverage_check": [
{"flag_id": "PK-FORWARD-DEP", "status": "confirmed", "finding_id": "PD-001", "note": ""},
{"flag_id": "PK-ANTECEDENT-BASIS", "status": "confirmed", "finding_id": "PD-002", "note": ""},
{"flag_id": "PK-MIXED-CLASS", "status": "confirmed", "finding_id": "PD-003", "note": ""},
{"flag_id": "PK-THIN-DEPENDENT", "status": "confirmed", "finding_id": "PD-004",
"note": "Claim 8 recites the parent and nothing else; 112(d)."},
{"flag_id": "PK-RELATIVE-TERM", "status": "set-aside", "finding_id": "",
"note": "\"Substantially steady\" has a yardstick in the specification - two percent over the window - so it is definite as supported. Reciting the number would be narrower but is not required."}
],
"artifact": {
"kind": "claims",
"filename": "claims-reviewed.txt",
"content": "1. A method for detecting incipient thermal runaway in a battery pack, the method comprising: ...\n\n4. The method of claim 1, wherein the acoustic sensor is a piezoelectric element.\n\n...\n"
},
"next_lane": {"lane": "claims", "reason": "Claim 9 needs redrafting as an apparatus claim rather than patching."},
"body": {
"claim_reviews": [
{"num": 1, "type": "independent", "depends_on": 0, "statutory_class": "method",
"definiteness": "clear", "one_sentence": true, "transition": "comprising",
"scope_note": "Covers operating any pack that opens a contactor on a gated acoustic burst rate.",
"verdict": "clean"},
{"num": 4, "type": "dependent", "depends_on": 5, "statutory_class": "method",
"definiteness": "questionable", "one_sentence": true, "transition": "none",
"scope_note": "Would narrow claim 1 to a piezoelectric sensor, if its dependency were proper.",
"verdict": "depends forward on claim 5"},
{"num": 9, "type": "independent", "depends_on": 0, "statutory_class": "mixed",
"definiteness": "indefinite", "one_sentence": true, "transition": "comprising",
"scope_note": "Recites an apparatus and then a measuring step, so infringement is undecidable.",
"verdict": "mixes statutory classes"}
],
"structure": {
"independent": 3, "dependent": 7, "total": 10, "max_depth": 2,
"multiple_dependent": [7],
"improper_dependencies": [{"claim_no": 4, "refers_to": 5, "problem": "forward reference"}],
"fee_thresholds": ["multiple-dependent claim surcharge for claim 7"]
},
"s112b": [
{"claim_no": 6, "term": "the shunt", "issue": "antecedent-basis", "severity": "high",
"why": "35 U.S.C. 112(b): no earlier claim introduces a shunt.", "fix": "Introduce it indefinitely in claim 6."},
{"claim_no": 9, "term": "further comprising the steps of", "issue": "mixed-class", "severity": "high",
"why": "35 U.S.C. 112(b) under IPXL: an apparatus claim reciting a method step leaves infringement undecidable.",
"fix": "Recite the measurement as a configured capability of the controller."}
],
"s112a": [
{"claim_no": 9, "term": "means for receiving an acoustic signal", "in_disclosure": true,
"note": "The specification discloses a piezoelectric element as the corresponding structure, which is what 112(f) construction will read the means to cover."}
],
"s112d": [
{"claim_no": 8, "problem": "adds no limitation beyond claim 1",
"fix": "Cancel it, or add the adaptive-threshold limitation from the specification."}
],
"rewrites": [
{"claim_no": 4, "before": "The method of claim 5, wherein the acoustic sensor is a piezoelectric element.",
"after": "The method of claim 1, wherein the acoustic sensor is a piezoelectric element.",
"what_changed": "dependency redirected to a preceding claim"},
{"claim_no": 9, "before": "... and further comprising the steps of measuring the pack current.",
"after": "... wherein the controller is further configured to measure the pack current.",
"what_changed": "method step converted to a configured capability, so the claim is wholly an apparatus"}
],
"clean_claims": [1, 2, 3, 5, 10]
}
}
The pipeline
The claims lane returns artifact.kind: "claims", and its
content is deliberately not JSON and not markdown: it is a plain numbered claim
set, 1. A method ... with a blank line between claims and nothing else —
no headings, no commentary, no numbering prefix beyond the number itself. That is exactly the shape
the review112 lane expects in claims_text.
So you feed the string straight back in. Two calls, the same
disclosure, and the drafted set goes through the 112 review that would otherwise wait
for a human to copy and paste it. That handoff is the point of the app. The
review112 run then returns artifact.kind: "claims" in turn, with
its rewrites applied in the same numbering, so a third call can review the reviewed set if a
practitioner has edited it.
Give the second call its own idempotency key. Same disclosure, different task: if
task is not in the key, the review run collides with the draft run and hands back the
draft lane's cached result.
# 1. Draft the set.
post /run '{"task":"claims","disclosure":"<the disclosure>","jurisdiction":"US"}' > draft.json
# ... poll /jobs/{id} until succeeded, then:
python3 - <<'PY' > claims.txt
import json
job = json.load(open("job.json"))
print(json.loads(job["data"]["output"]["output"])["artifact"]["content"], end="")
PY
# 2. Review it. claims_text is the artifact content, verbatim.
python3 -c "import json;print(json.dumps({
'task': 'review112',
'disclosure': open('disclosure.txt').read(),
'claims_text': open('claims.txt').read(),
'jurisdiction': 'US'}))" > review-payload.json
curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: patent-desk:review112:$HASH:1" \
-d @review-payload.json
disclosure = open("disclosure.txt").read()
# 1. Draft the set.
draft_payload = {"task": "claims", "disclosure": disclosure, "jurisdiction": "US"}
draft = run_and_wait(draft_payload, idem_key(draft_payload))
assert draft["artifact"]["kind"] == "claims", draft["artifact"]["kind"]
claims_text = draft["artifact"]["content"] # "1. A method ...\n\n2. ...\n"
# 2. Review the very same string. No reformatting, no re-numbering.
review_payload = {
"task": "review112",
"disclosure": disclosure,
"claims_text": claims_text,
"jurisdiction": "US",
}
review = run_and_wait(review_payload, idem_key(review_payload))
print(review["posture"], "-", review["verdict"])
print("clean:", review["body"]["clean_claims"])
for r in review["body"]["rewrites"]:
print(f" claim {r['claim_no']}: {r['what_changed']}")
# The reviewed set, rewrites applied, in the same numbering.
open("claims-reviewed.txt", "w").write(review["artifact"]["content"])
const disclosure = readFileSync("disclosure.txt", "utf8");
// 1. Draft the set.
const draftPayload = { task: "claims", disclosure, jurisdiction: "US" };
const draft = await runAndWait(draftPayload, idemKey(draftPayload));
if (draft.artifact.kind !== "claims") throw new Error(draft.artifact.kind);
const claimsText = draft.artifact.content; // "1. A method ...\n\n2. ...\n"
// 2. Review the very same string.
const reviewPayload = {
task: "review112",
disclosure,
claims_text: claimsText,
jurisdiction: "US"
};
const review = await runAndWait(reviewPayload, idemKey(reviewPayload));
console.log(review.posture, "-", review.verdict);
console.log("clean:", review.body.clean_claims);
for (const r of review.body.rewrites) {
console.log(` claim ${r.claim_no}: ${r.what_changed}`);
}
writeFileSync("claims-reviewed.txt", review.artifact.content);
disclosureBytes, _ := os.ReadFile("disclosure.txt")
disclosure := string(disclosureBytes)
// 1. Draft the set.
draftPayload := map[string]any{
"task": "claims", "disclosure": disclosure, "jurisdiction": "US",
}
draft := runAndWait(draftPayload, keyFor(draftPayload, 1))
if draft.Artifact.Kind != "claims" {
panic("unexpected artifact kind: " + draft.Artifact.Kind)
}
claimsText := draft.Artifact.Content // "1. A method ...\n\n2. ...\n"
// 2. Review the very same string.
reviewPayload := map[string]any{
"task": "review112",
"disclosure": disclosure,
"claims_text": claimsText,
"jurisdiction": "US",
}
review := runAndWait(reviewPayload, keyFor(reviewPayload, 1))
fmt.Println(review.Posture, "-", review.Verdict)
os.WriteFile("claims-reviewed.txt", []byte(review.Artifact.Content), 0o644)
String disclosure = Files.readString(Path.of("disclosure.txt"));
// 1. Draft the set.
String draftPayload = """
{"task": "claims", "disclosure": %s, "jurisdiction": "US"}
""".formatted(JsonUtil.quote(disclosure));
String draft = runAndWait(draftPayload, "patent-desk:claims:" + digest + ":1");
String claimsText = JsonUtil.path(draft, "artifact", "content");
// 2. Review the very same string.
String reviewPayload = """
{"task": "review112", "disclosure": %s, "claims_text": %s, "jurisdiction": "US"}
""".formatted(JsonUtil.quote(disclosure), JsonUtil.quote(claimsText));
String review = runAndWait(reviewPayload, "patent-desk:review112:" + digest + ":1");
System.out.println(JsonUtil.path(review, "verdict"));
Files.writeString(Path.of("claims-reviewed.txt"), JsonUtil.path(review, "artifact", "content"));
disclosure = File.read("disclosure.txt")
# 1. Draft the set.
draft_payload = { "task" => "claims", "disclosure" => disclosure, "jurisdiction" => "US" }
draft = run_and_wait(draft_payload, idem_key(draft_payload))
raise draft["artifact"]["kind"] unless draft["artifact"]["kind"] == "claims"
claims_text = draft["artifact"]["content"] # "1. A method ...\n\n2. ...\n"
# 2. Review the very same string.
review_payload = {
"task" => "review112",
"disclosure" => disclosure,
"claims_text" => claims_text,
"jurisdiction" => "US"
}
review = run_and_wait(review_payload, idem_key(review_payload))
puts "#{review['posture']} - #{review['verdict']}"
puts "clean: #{review['body']['clean_claims'].join(', ')}"
File.write("claims-reviewed.txt", review["artifact"]["content"])
<?php
$disclosure = file_get_contents("disclosure.txt");
// 1. Draft the set.
$draftPayload = ["task" => "claims", "disclosure" => $disclosure, "jurisdiction" => "US"];
$draft = run_and_wait($draftPayload, idem_key($draftPayload));
if ($draft["artifact"]["kind"] !== "claims") {
throw new RuntimeException($draft["artifact"]["kind"]);
}
$claimsText = $draft["artifact"]["content"]; // "1. A method ...\n\n2. ...\n"
// 2. Review the very same string.
$reviewPayload = [
"task" => "review112",
"disclosure" => $disclosure,
"claims_text" => $claimsText,
"jurisdiction" => "US",
];
$review = run_and_wait($reviewPayload, idem_key($reviewPayload));
echo "{$review['posture']} - {$review['verdict']}\n";
file_put_contents("claims-reviewed.txt", $review["artifact"]["content"]);
var disclosure = await File.ReadAllTextAsync("disclosure.txt");
// 1. Draft the set.
var draftPayload = new { task = "claims", disclosure, jurisdiction = "US" };
var draft = await RunAndWaitAsync(draftPayload, KeyFor("claims", disclosure, ""));
var artifact = draft.GetProperty("artifact");
if (artifact.GetProperty("kind").GetString() != "claims") throw new Exception("kind");
var claimsText = artifact.GetProperty("content").GetString();
// 2. Review the very same string.
var reviewPayload = new
{
task = "review112",
disclosure,
claims_text = claimsText,
jurisdiction = "US"
};
var review = await RunAndWaitAsync(reviewPayload, KeyFor("review112", disclosure, claimsText));
Console.WriteLine(review.GetProperty("verdict"));
await File.WriteAllTextAsync("claims-reviewed.txt",
review.GetProperty("artifact").GetProperty("content").GetString());
run_and_wait is step 5 wrapped in a function: post to /run with the
idempotency key, poll GET /jobs/{job_id} until terminal, and return
json.loads(status["output"]["output"]).
Notes that will save you a support round trip
- The output is one JSON object, and you should still strip a stray code fence.
The contract says no prose and no fence; a client that trims a leading
```jsonand a trailing```before parsing costs three lines and removes a whole class of failure. Parse first, then checklane. - The run body is not wrapped in an
inputkey.POST /run,POST /run-streamandPOST /estimateall take the input object itself:{"task": ..., "disclosure": ...}at the top level. Wrapping it produces aVALIDATION_ERRORnamingtaskas missing, which reads like the wrong problem. - There is no slug header — not
X-Slug, not anX-App-...variant, not anything. The only headers on any endpoint areAuthorization,Content-Typeand, on the two run endpoints,Idempotency-Key. The slug is named once, in thePOST /guestbody. A bogus custom header is ignored rather than rejected, so sending one looks like it works and then explains nothing when something else breaks. review112withoutclaims_textreturnsposture: "blocked", with the reason inverdict, what is needed inopen_questionsand empty body arrays. It does not invent a claim set to review, and it still costs a run. Check the field client-side first.- The model has no search access and will not return prior-art references. By
design, and enforced in the prompt: no patent number, no publication, no application number, no
author, no date, not even as an illustration. The
priorartlane produces the search to run. If you need results, run the queries it gives you in a real search interface. - Two lanes over one disclosure are two runs. Include
taskin the idempotency key or the second lane returns the first lane's cached result. - Check
lane_inferred. If it istrue, yourtaskfield did not arrive or was not recognised and the model chose a lane for you. Treat the response as suspect rather than as an answer to the question you asked. - Reconcile
coverage_checkagainst the flags you sent, both ways: a missing entry is an unreconciled finding, and an entry for an id you did not send is an invented one. Both are cheap to assert and both are real defects. countsandstructuremust agree with the claim set. Theclaimslane'scountshas to matchclaim_set, andreview112'sstructurehas to match the pasted set; a claim cannot be in bothclean_claimsands112b. The page checks all three, and so should you.- Clip a disclosure from the middle and a claim set on whole-claim boundaries. A
disclosure carries its definitions early and its claim support late, so the middle is what goes.
Half a claim is not a claim: a truncated claim generates antecedent-basis flags that are artefacts
of the cut. Send
clip_notewhenever you cut anything, and expectartifact.kind: "none"withposture: "blocked"rather than a claim set with a silently renumbered gap. - Do not send anything you would not want restated. A disclosure is unpublished and usually confidential. The prompt restrains how much is echoed back, but the safest input is one with no credentials, keys or personal identifiers in it at all.