# 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` ```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` ```json { "tool": "data.aggregate", "resource_id": "", "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. ```bash 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":"","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/`, 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`, `completed` or `failed`. - `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 absolute `metadata_expires_at`. - `result_expires_at`, `metadata_expires_at`, `created_at`, `finished_at`: Unix seconds; pending work already declares its body deadline. - `error_code` and `error`: 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/` 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](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/). ## 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. ```bash 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:8000 ``` Download 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: ```sh 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 --resume ``` The 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.