# Grainmark > Capture-time media provenance. Seal a photo or video when you record it. Later, anyone with the file and its sidecar can ask whether a copy is still that media. Grainmark is the product (was Proofsheet until 2026-09-20). The protocol underneath is VRC-48M. This is not a wild-stream deepfake detector and it does not scan the internet for synthetic video. A match is an unsigned visual comparison against a sidecar or a seal this host still holds — not a camera signature, or a claim that the scene was never generated. A confirmed timestamp proves the seal existed no later than Bitcoin block time T. It does not prove who captured the media, when it was captured, or that it was sealed at capture. Canonical host: https://grainmark.app Alias (do not 301 POST /api/*): https://proofsheet.rick3.dev Source: https://github.com/rrhoopes3/VRC48m Contact: hello@rick3.dev ## Pages - [Home / demo](https://grainmark.app/): seal, verify a copy, or compare two files - [Verify tab](https://grainmark.app/?tab=verify): same demo, Verify panel - [Studio](https://grainmark.app/studio): editor + seal + sidecar download - [Privacy](https://grainmark.app/privacy): what is uploaded, what is kept, private vs published - [This file](https://grainmark.app/llms.txt): agent discovery (also /llm.txt and /.well-known/llms.txt) - [MCP discovery](https://grainmark.app/.well-known/mcp.json): endpoint, protocol versions, public tools - [Connector brief](https://grainmark.app/connectors/muse.md): written for an agent building against this host ## What a green / AUTHENTIC result means - Full-frame visual descriptors on the overlapping interval (identity / modest same-aspect scale). - Honest recompression (JPEG ≥ Q40), brightness, blur, typical transcode. - `PARTIAL` is three stored probe JPEGs on a crop/reframe — not frame-by-frame coverage. Do not treat PARTIAL as a full match. - Audio and playback timing are not compared. - `confidence` is a matching fraction under threshold 0.16, not P(authentic). ## API (JSON envelope `{ok, data}` / `{ok, error}`) Base `https://grainmark.app`. CORS `*`. Anonymous access is supported. Send a key as `Authorization: Bearer ` or `X-API-Key`. Keys start with `gm_live_`; scopes are `seal:write`, `verify:read`, and `anchor:read`. The iOS app sends `X-Grainmark-Install` and does not use a key. A use is one seal, verify, or compare. The allowance follows the file suffix the decoder uses, and a video the plan does not include is refused before analysis. Lookups, get_seal, and the published list stay on the light budget and are not uses. A day is a fixed 24-hour window starting at the first use. It does not reset at midnight. Hourly limits are a ceiling against abuse, not reserved capacity. | | Anonymous web (per IP) | iOS app (per install) | Free key ($0) | Pro key ($9/month) | |---|---|---|---|---| | Photo uses | 10/day | 10/day | 10/day | no daily cap; 240/hour | | Video uses | not included | 3/day | not included | 30/hour | | Light API calls | 240/hour | 240/hour | 3600/hour | 3600/hour | | Total request body | 25 MiB | 25 MiB | 200 MiB | 200 MiB | The iOS columns are 10 photos/day and 3 videos/day per install. A per-address backstop of 50 photos/day and 15 videos/day is not a replacement for the 10 photos/day and 3 videos/day per install. A legacy key minted without a plan keeps one combined heavy budget. The server-wide upload ceiling remains 200 MiB. Limits also apply to requests without `Content-Length`. Rate limits return `429` with `Retry-After`; a video the plan does not include returns `402`. Oversized requests return `413`. Media caps: 20 s, 600 frames, stills up to a 3840-pixel edge and 3840×2160 total pixels (landscape or portrait), video up to a 1920-pixel edge and 1920×1080 total pixels, 2 concurrent jobs (`429`). Larger phone photos must be resized before uploading; the server does not silently downscale them. New seals are **private until published**. Sharing a sidecar shares three embedded JPEG probes. | Method | Path | Notes | |--------|------|--------| | POST | `/api/vrc48m/anchor` | multipart `file`; optional `publish=true`, `source_url`. Returns `anchor_id`, `sidecar` (keep this), `access_token` (once). The HTTP envelope is not a sidecar. | | POST | `/api/vrc48m/verify` | multipart `file` plus either sidecar file `anchor` **or** `anchor_id` (unpublished needs `token`). | | POST | `/api/vrc48m/compare` | multipart `original` + `suspect`. No registry. | | GET | `/api/vrc48m/anchors` | published seals only; labels are `Anchor `, not filenames. | | GET | `/api/vrc48m/lookup` | `?sha256=` or `?url=`; published only. | | GET | `/api/vrc48m/anchor/` | published metadata; no probes. | | GET | `/api/vrc48m/anchor//download` | portable sidecar; published or `token`. The bytes stay fixed for the life of the seal. | | GET | `/api/vrc48m/anchor//timestamp` | `queued`, `pending`, or `confirmed`; block height and time once confirmed (`block_time_checked_against`: public explorers). Same access as the sidecar. Seal token in a header or form, not the query string. | | GET | `/api/vrc48m/anchor//timestamp.ots` | detached OpenTimestamps proof, once the hourly batch has submitted it. | | POST | `/api/vrc48m/anchor//publish` | form/query/`Authorization: Bearer` token. | Private-seal access is separate from the API key. Send the create-time `access_token` as form `token`, query `?token=`, or a legacy `Authorization: Bearer` seal token. With an API key in the Bearer header, send the seal token in the form; alternatively use `X-API-Key` alongside a Bearer seal token. An API key alone does not unlock private seals. Sidecar-only verify works on any host that never held the seal. Do not add fetch-by-URL. Live Socket.IO capture is off unless `VRC48M_LIVE_STREAM=1` (public host must not set that). ## Optional curl ``` curl -sS -F "file=@clip.mp4" https://grainmark.app/api/vrc48m/anchor # keep .data.sidecar and .data.access_token curl -sS -F "file=@clip.mp4" -F "anchor=@clip.grainmark.json" https://grainmark.app/api/vrc48m/verify ``` ## Key facts - Python package remains `vortexchain`; HTTP paths remain `/api/vrc48m/*`. - iOS bundle id is still `proofsheet.rick3.app` until a new App Store listing. - MCP server: `POST https://grainmark.app/mcp` (stateless JSON-RPC 2.0, no SSE, no session id). Its separate budget is 60 calls/minute per valid API key, or per address anonymously; discovery/read tools work without a key. With a key, `begin_seal`/`complete_seal` and `begin_verify`/`complete_verify` seal and verify in two steps: `begin_*` returns a one-time upload URL, you POST the bytes there with the same key, then call `complete_*`. Grainmark never fetches media from a URL. Tool classes: the three lookup tools and the verify pair are reads; `begin_seal` is a write; `complete_seal` is a sensitive write (it can publish a seal, which cannot be undone). MCP annotations in `tools/list` encode read versus write only; the sensitive designation is in the connector brief. - Grey-cloud DNS on purpose: Cloudflare's proxy caps uploads at 100 MB; keyed REST requests may use up to 200 MiB, while anonymous requests are capped at 25 MiB.