MARGINNOOK / DEVELOPER PREVIEW
HTTP API reference
The local R2 resource, quote, job, evidence and recovery contract.
Developer preview. Hosted API is not live yet. Explore fixed synthetic samples; anonymous uploads, hosted execution and payments remain closed.
Download authoritative Markdown
MarginNook API — local R2 contract v0.1
Hosted API is not live yet. https://marginnook.com/ currently serves the product website, not these endpoints. The implementation below runs locally at http://127.0.0.1:8000. Public hosting, cloud persistence and checkout require separate release verification.
Design
Upload input from the client runtime. Give the model a private resource handle and a small set of tool arguments. Keep large source files out of model context. Every result declares its scope and incomplete output; the client can download a full returned result and the agent can read bounded evidence fragments.
No registration or API-key application is required for a first upload. The service creates an anonymous credential automatically. The response contains X-MarginNook-Client and a same-site HTTP-only cookie. A client should persist either credential securely. An unknown supplied credential is rejected, not silently treated as a new identity.
Resource IDs grant reading and free execution against that resource; they cannot delete data, disclose full principal usage or spend future paid balances. Job IDs grant independent redacted status access for 31 days; body access ends with the source family. The automatic client credential grants management deletion and principal usage access. Do not log or share them publicly. This design allows MCP clients to use a supplied handle without assuming that every client preserves custom headers or cookies. IP addresses are not user identities.
Upload a resource
POST /v1/resources, Content-Type: application/json
{
"format": "json_rows",
"content": [
{"category": "books", "amount": "12.50"},
{"category": "books", "amount": "7.25"}
]
}Supported formats:
| Format | Content field | Scope |
json_rows | JSON array or JSON text in content | Array of row objects |
csv | UTF-8 text in content | Header row required |
text | UTF-8 text in content | Paragraph boundaries retained |
html | HTML text in content | Local extraction; no URL fetching |
docx | Base64 bytes in content_base64 | Ordinary text-bearing DOCX; limited ZIP expansion |
junit_xml | XML text in content | Supported JUnit/xUnit reports; DTD/entities refused |
PDF, OCR, filesystem paths, arbitrary URLs and source-code execution are not supported by this API release. Uploading a resource is not a promise that every file in an allowed format can be parsed.
The HTTP 201 response contains id, format, size_bytes, created_at and expires_at. Times use Unix seconds. Maximum resource size is 2 MiB; JSON request bodies have a separate 3 MiB bound, allowing base64 overhead. The entire resource family expires 24 hours after the original upload. Derived text, returned JSON and inline job bodies inherit that same absolute deadline, including when a task finishes near expiry.
Quote before execution
POST /v1/quote
{
"tool": "data.aggregate",
"resource_id": "<private-resource-handle>",
"arguments": {"group_by": "category", "value_field": "amount"}
}The response reports credits, the format/size boundary, source expiry, validation_stage=parameters_and_resource_metadata and input_parsed=false. Only the validated management credential receives the full principal allowance. Ordinary resource capabilities receive no principal usage totals. Required fields, strict integer types (booleans and numeric strings refused), defaults and ranges are validated identically before quote/admission in HTTP and MCP. A quote neither reserves nor charges credits. Actual parsing can still fail; failed processing releases any reserved credits.
Submit and retrieve a job
POST /v1/jobs with the same JSON. Set Idempotency-Key to a client-generated unique value of 8–160 characters for this logical task. Persist it across network retries.
curl -X POST http://127.0.0.1:8000/v1/jobs \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: example-aggregate-001' \
--data '{"tool":"data.aggregate","resource_id":"<private-resource-handle>","arguments":{"group_by":"category","value_field":"amount"}}'New jobs return HTTP 202 with an ID and status=pending. The client runtime polls GET /v1/jobs/<private-job-handle>, without asking the language model to generate repeated poll calls. A reused terminal job returns its existing result. The same principal/key with different normalized logical arguments produces a conflict.
The job result uses:
status:pending,completedorfailed.credits: the logical task's quoted amount.reserved_credits: credits currently held for pending work.charged_credits: credits charged for completed work; zero after failure.result.output: the structured operation result when it fits the inline boundary.result.output_included: whether that output was included inline.result.result_resource: metadata for the complete returned JSON result.result.source_resource_id: the original uploaded input.result.normalized_source_resource: for document search, the actual normalized text used for source coordinates.result_expired: body access has ended; metering/status remain until the absolutemetadata_expires_at.result_expires_at,metadata_expires_at,created_at,finished_at: Unix seconds; pending work already declares its body deadline.error_codeanderror: stable failure classification, including after error body removal. Failed work charges zero credits.
Inline results above 8,000 characters are represented by resource metadata and instructions. The complete returned JSON is downloadable. Core operations themselves apply explicit pagination and a 128 KiB result bound; downloading that returned JSON does not turn a paginated result into an unbounded full-dataset result. Consult incomplete, next_offset, omitted_items and source_read_required. A changed operation/query that computes a new page is quoted as a new job; reading an already returned result is free.
GET /v1/resources/<private-resource-handle> downloads the exact stored content as an attachment. DELETE requires the resource owner’s X-MarginNook-Client management credential or same-site cookie. It revokes and deletes the entire family, even when targeting a derived member; response scope=resource_family, plus checkpoint_pending when a concurrent SQLite reader prevents physical WAL truncation; cleanup retries it. Authorization is revoked immediately. All derived resources and inline job bodies become unavailable. Completed usage remains charged; pending work fails and releases credits. Publishing output checks family validity inside the same transaction. Previously downloaded client copies cannot be recalled. The private Cloudflare SQLite candidate has provider-managed point-in-time history for up to 30 days; active-data deletion does not claim immediate erasure of that history. Private-ingress synthetic checks verify Cache-Control: no-store on successful reads and revoked-resource errors, immediate family revocation, and retained task metadata without inline content; see evidence/cloud-private-cache-delete-retest.json. These checks do not prove provider backup erasure or complete final hosted acceptance. See provider SQLite storage documentation.
Three operations
data.aggregate — 1 credit
Input resource: json_rows or csv.
Arguments: group_by, value_field; optional limit (1–1,000; default 100) and offset (default 0). Field names currently use ASCII letters/digits/underscores, begin with a letter/underscore, and have at most 64 characters. Maximum input: 10,000 rows. CSV duplicate/empty headers and every row width mismatch are rejected instead of silently overwriting fields. Parsing happens in the bounded worker; quote validates parameters and metadata, not file correctness. Arbitrary SQL is never accepted.
Returns grouped counts and sums, stable group references, input scope and explicit pagination. Decimal strings go directly to Decimal. sum is an exact decimal string within the supported 38-digit precision/range. Missing fields, null/non-numeric amounts, NaN, infinity and invalid numeric text fail instead of silently changing values. A JSON float cannot recover precision lost before the service received it; use decimal strings where that matters.
document.search — 2 credits
Input resource: text, html or docx.
Arguments: query; optional limit (1–10; default 5), offset (default 0). Maximum query size is 512 bytes and 24 content keywords.
Returns complete matching paragraphs with section IDs and start_char/end_char coordinates. These are Unicode character offsets into normalized_source_resource, not UTF-8 byte offsets. Literal words are quoted before an FTS5 any-term query; caller input is not executed as FTS syntax.
absence_proven is always false. No match does not prove that the document lacks the answer. incomplete=false describes returned matches, not semantic recall or full-document understanding. HTML extraction can omit content; DOCX conversion covers text and may omit images or other nontext information. Those boundaries are carried in scope.
report.failures — 1 credit
Input resource: junit_xml.
Arguments: optional max_cases (1–100; default 10), offset (default 0).
Returns counts and cases containing failure/error events, traces and relevant associated output. Multiple events in one case remain separate. Failed-case counts and error/failed-event counts are distinct. Nested expected/actual evidence is retained. Duplicate case identities and retries are not silently deduplicated. This is a structured report parser, not a generic log summarizer.
MCP
Local endpoint: http://127.0.0.1:8000/mcp/.
Implemented with the official Python SDK, Streamable HTTP, stateless transport and structured results. Tools:
| Tool | Purpose |
aggregate | Call data.aggregate using a private resource handle |
search_document | Query a document resource |
test_failures | Inspect a report resource |
read_evidence | Read a bounded text/result fragment without a new job charge |
read_evidence(resource_id, offset=0, length=4000) uses character offsets, with at most 8,000 characters per read. It explicitly labels fragments and supplies the next offset; a fragment of JSON is returned as text, not as a falsely complete JSON document. For DOCX, read the returned normalized text resource or download the original outside model context.
The current SDK supports the locally tested 2025-11-25 and 2026-07-28 paths. Newer protocol requests use the SDK's version/capability metadata and method headers. Use a compatible SDK rather than inventing headers. Private hosted Inspector CLI/GUI and MCPJam success/evidence calls have been verified. A modern admission-error compatibility failure found in MCPJam was repaired and retested on the private hosted service. Public availability is still gated.
Do not assume that content and structuredContent should both be injected into a model. The SDK may return both representations. A client should deliberately choose the representation it consumes and record actual usage. The wire JSON size is not necessarily the model context size.
Allowance and errors
Default anonymous allowance: 50 credits per rolling 24 hours and 1,000 per rolling 30 days. Uploads, quote reads, status polls, existing-result downloads, valid idempotent retries and evidence fragments do not create new billable jobs.
Local capacity is independent of credits: 100 execution attempts/rolling 24h, 256 MiB logical bodies, 8 pending jobs globally, 2 per principal, 16 MiB pending input, 100 new anonymous principals/24h, 100 uploads and 50 MiB input/24h, 20 active originals/principal and 500 resources including derivatives. Every HTTP/MCP transport is counted; identified principals have a 20-token bucket refilling at 1/s. A separate bucket serves verified deletion/status recovery. Global ordinary requests stop at 9,000/24h; 1,000 additional control requests are reserved. Ordinary output is bounded by 100 MiB/24h and control output by a separate 8 MiB/24h. These are application limits, not proof of provider-level capacity or billing protection. Failed work releases user credits but still consumes shared execution capacity. Anonymous identity reset cannot be prevented perfectly; shared limits protect the budget. Paid checkout is disabled.
| HTTP status/code | Meaning |
400 INVALID_INPUT | Unsupported format/arguments or malformed input |
404 NOT_FOUND | Resource, job or credential absent/expired/invalid |
408 REQUEST_TIMEOUT | Request-body reception exceeded 15 seconds |
409 IDEMPOTENCY_CONFLICT | A key already represents another request |
413 INPUT_TOO_LARGE | Resource or request exceeds a bound |
429 FREE_QUOTA_EXHAUSTED | This anonymous client's allowance is exhausted |
503 PERSISTENCE_UNAVAILABLE | Authoritative state could not be read safely; retry without changing the logical key |
503 PLATFORM_CAPACITY | Shared service capacity is unavailable, independently of the client's allowance |
Errors carry code, category, retryable and retry_after; unavailable credentials/handles are deliberately indistinguishable. MCP tool errors expose the same machine envelope in structuredContent; pre-dispatch validation and capacity failures also set isError=true. Resource-only quota errors omit full principal usage. A completed HTTP job may contain status=failed and a processing error, with charged credits zero. Do not treat finding test failures as execution failure: successfully parsing an unsuccessful test run is a delivered result.
Runnable clients
These repository examples were executed against a real localhost server on macOS arm64 / Python 3.12 and Node 24. They make no model calls and do not demonstrate remote GUI compatibility. Downloadable copies are also published at the static site’s /examples/ paths.
python examples/http_client.py http://127.0.0.1:8000
node examples/http_client.mjs http://127.0.0.1:8000
# Uses the project's official MCP Python SDK dependencies:
PYTHONPATH=examples OTEL_SDK_DISABLED=true python examples/mcp_client.py http://127.0.0.1:8000
node examples/mcp_client.mjs http://127.0.0.1:8000Download both Python files into the same folder for the MCP example. Download exact_json.mjs alongside either Node example. Node 24 examples preserve integer JSON tokens outside the safe Number range as BigInt and encode them back as numeric tokens when saving or sending; native response.json() / JSON.parse() can round those integers. Decimal sums remain strings. HTTP scripts require no additional client packages. Node MCP uses the tested 2025-11-25 JSON transport; Python SDK negotiates the current supported path. No MarginNook pip/npm SDK has been published.
Private recovery files are saved under .runtime/ with owner-only permissions. Keep credentials, resource/job handles and logical keys out of model context and public logs. HTTP examples wait up to 180 seconds with backoff, jitter and Retry-After; quota exhaustion is reported instead of prompting payment. Do not retry an uncertain upload automatically. Retain the saved job ID/key after timeout, then resume GET /v1/jobs/{id}; a wait timeout does not cancel the job. Task submission retries must use that same key. Python MCP hands a pending response to the HTTP runtime waiter. Select one structured representation for model input, not both SDK representations.
Application cleanup runs every 15 minutes while the process is awake; accesses enforce expiry immediately. Sleeping-host scheduling and physical provider cleanup still require hosted verification. Redacted job metadata and hashed logical keys are removed at admission+31 days; inactive anonymous credentials are removed after 31 days without retained references. No wallet or payment records exist.
Production boundary
Local SQLite proves transaction and restart behavior on a persistent local file. It does not prove that a free cloud container keeps its disk. The hosted version requires verified persistent metadata/resources, multi-instance consistency, 24-hour retention measured using actual elapsed hosted time, private handles, deletion semantics, cold-start behavior and correct Host/Origin protection. Until those checks pass, do not expose this build as an established public service or accept prepaid money.
JSON objects with duplicate keys or nonfinite numbers are rejected as INVALID_INPUT; this includes uploaded JSON-row text when parsed for execution. Recognized subjects consume their request rate bucket even when their parameters or JSON are invalid.
Runtime retry boundaries
MCP waits up to 22 seconds before returning pending status. Clients persist recovery information before submission and share a 180-second deadline across submission and waiting. They respect retryable: false. Automatic retries cover reads, deletion, read-only quotes and keyed submissions; uncertain uploads are not automatically retried. State saturation returns platform capacity errors separately from free credits. Public hosting remains unavailable.
The server bounds active HTTP transports to four business requests and two control requests. Request bodies must arrive within 15 seconds; timeout returns HTTP 408 / REQUEST_TIMEOUT. Control and MCP bodies are limited to 64 KiB; other bodies to 3 MiB. This does not change the 2 MiB resource limit. Local limits do not establish final-host memory or availability guarantees.
Control slots are reserved for job-status GET requests and credentialed resource DELETE requests. Authorization is checked before receiving a control request body. Unauthorized management attempts count against the verified caller, not the resource owner. This local protection does not guarantee availability during a provider outage.
Python examples send compact UTF-8 JSON rather than ASCII escapes, so valid non-ASCII inputs do not incur avoidable wire-size expansion. HTTP clients serialize the request once before retrying; expired task deadlines stop before sending another request. Python rejects non-finite JSON values before transmission. The separate 3 MiB request limit still applies to necessary JSON escaping.
HTTP examples explicitly read the original CSV as raw bytes before deletion. Python Client.request(..., raw=True) preserves binary content; the Node example uses arrayBuffer(). All four examples reject redirects rather than forwarding management credentials or resource capabilities. JSON media-type names are accepted case-insensitively with parameter whitespace; payload parsing remains strict.
Private hosting qualification: non-loopback API startup now requires an operator-generated MARGINNOOK_STAGING_TOKEN (32–128 URL-safe characters). Only GET /health is available without it; HTTP/MCP routes require X-MarginNook-Staging before request-body handling. The four client examples read the same optional environment variable without saving it in recovery state. This operator credential does not replace resource or management authorization, and does not open anonymous beta. Do not include it in URLs, public documents, model context or customer profiles. Public beta startup remains unavailable pending H1–H10.
Resume an interrupted example with its original endpoint and private working directory:
python examples/http_client.py http://127.0.0.1:8000 --resume
node examples/http_client.mjs http://127.0.0.1:8000 --resume
python examples/mcp_client.py http://127.0.0.1:8000 --resume
node examples/mcp_client.mjs http://127.0.0.1:8000 --resumeThe runtime reads its existing .runtime/*client.json file. A known job is read directly; an uncertain submission is recovered with the saved canonical request and the same idempotency key through HTTP. No new upload/key is generated. Failed/expired jobs surface their recorded error; an uncertain upload without a saved task cannot be retried automatically. MCP examples reconnect/discover and read evidence after HTTP task recovery. The normal demo deletes its resource family at completion, so resuming an already deleted demo reports unavailable content. Recovery state is private, endpoint-bound and written with mode 0600. Keep it outside model context and Git.
Execution can terminate with OUTPUT_TOO_LARGE (category output, retryable: false, with limit_bytes and requested_bytes) when a processed body exceeds the supported output bound. Reserved free credits are released; the failed task remains readable through its independent task capability. Quotes validate parameters and resource metadata without parsing the complete input or guaranteeing expanded output will fit. Provider capacity failures retain PLATFORM_CAPACITY and retry information instead of suggesting payment.