Request for Development — webGCP L0 MVP

Audience: anyone building a conformant L0 webGCP server. Implementer-facing.

This is not the spec. The spec is the normative authority at https://webgcp.org/spec/v0.1/ (markdown source: webgcp-v01-final.md). This doc is the implementer's guide — what to build, in what order, with concrete pointers to a working reference. Read the spec for the rules; read this for the construction sequence.

What "MVP" means here: the minimum a single team needs to ship to claim L0 (or L0-restricted-domain) per the spec. Once L0 ships, you're a conformant implementation; you're listed at https://webgcp.org/implementations/; and your 3-month production-soak clock (Step 3 of the standards playbook) starts ticking.


1. What L0 MVP comprises

Five artifacts. Three live, two on disk.

# Artifact Lives at Spec section
1 DNS SVCB record _webgcp.<your-host> (TYPE 64) §5.1.1
2 Server descriptor (JSON) https://<your-host>/.well-known/webgcp §5.1
3 One KB manifest (JSON) https://<your-host>/<path>/manifests/<kb-id>-v0.1.json (path is operator-chosen) §5.2
4 One bundle contract (JSON) https://<your-host>/<path>/bundles/<contract>-v0.1.json (path is operator-chosen) §5.3
5 Dynamic query endpoint (POST handler) https://<your-host>/webgcp/v0/query §6.3, §6.4

Plus two conformance fixtures (downloaded from webgcp.org, not authored by you) that you run against your endpoint:

All three must pass. (L0-003 fails if you're under a restricted DNS zone like .web.app / .github.io / .netlify.app / .vercel.app — in which case you claim L0-restricted-domain instead, per §5.1.1.)


2. Build checklist (in order)

Each step has a clear "done" signal. Don't skip ahead — later steps assume earlier ones.

2.1 Pick a host you control

You need a domain (or subdomain) where you control authoritative DNS and can serve HTTPS. Both are required.

Acceptable hosts (full L0): - news.example.com — apex or subdomain of a domain you've registered - acme-skills.io — apex - Anything where you can add an SVCB record at _webgcp.<host> and serve HTTPS

Restricted-domain hosts (L0-restricted-domain only): - *.web.app / *.firebaseapp.com (Firebase Hosting auto-domains) - *.github.io (GitHub Pages) - *.netlify.app, *.vercel.app, *.pages.dev - Any platform that doesn't let you manage authoritative DNS records at your hostname

Done when: you can curl -sI https://<your-host>/ and get an HTTPS 200 (any content). DNS records you control can be added.

2.2 Pick a backing store

The protocol is implementation-neutral. Acceptable backing stores at L0: - Bundled fixture data (Python dict, JSON file, etc.) — the simplest demo case - SQLite, Postgres+pgvector, MySQL — small structured datasets - Firestore, DynamoDB, MongoDB — document stores - BigQuery, Snowflake, Databricks — analytical / large-scale - A graph DB (Neo4j, Spanner Graph, BigQuery Property Graph)

L0 doesn't require vector search or graph traversal — direct lookup is enough. That's L1 territory.

Done when: you can write a function fetch(query, filter) → list_of_records that returns matching records for a topic-filtered text query, against any of the above.

2.3 Author the manifest (§5.2)

The manifest describes what your KB is for and when it applies. This is the meta-knowledge layer.

Required top-level fields (full schema at https://webgcp.org/schemas/manifest/v0.1.json):

Worked example: https://the-ai-news.web.app/webgcp/manifests/news-kb-v0.1.json. Copy its structure, replace the AI-news-specific content with your domain's content.

The applicability predicate format. Each entry in in_scope_when[] and out_of_scope_when[] is a tagged JSON object with a predicate field naming the check, plus type-specific fields:

{
  "applicability": {
    "in_scope_when": [
      { "predicate": "topic_in", "values": ["ai-research", "machine-learning", "llms"] },
      { "predicate": "time_range", "min": "2026-01-01", "max": "now()" },
      { "predicate": "language_eq", "value": "en" }
    ],
    "out_of_scope_when": [
      { "predicate": "topic_outside_domain", "note": "Non-AI topics (sports, politics, finance)" },
      { "predicate": "private_facts", "note": "Queries about non-public individual data" }
    ]
  }
}

The predicate names are operator-defined — there is no closed enum at v0.1. You name predicates after what they check. The note field is human-readable context for agents deciding whether to call. What matters is that your query endpoint enforces these declarations (see §2.5 below).

Common trip: applicability.out_of_scope_when is load-bearing. The whole point of the manifest is that an agent can tell — before calling — whether your KB will return a meaningful answer or out_of_scope. Don't leave it empty. A manifest without out_of_scope_when entries is a promise to answer everything — which is almost never true.

Done when: you publish the manifest at a stable URL and curl -s <url> | jq .kb.id returns the right URN.

2.4 Author the bundle contract (§5.3)

The contract is what an agent declares it needs. Defines the typed response shape.

Required top-level (full schema at /schemas/bundle/v0.1.json):

Per-field MUSTs (§5.3, §5.5, §5.6): - primitive_hint — one of direct_lookup | hierarchical_tree | vector_hybrid | graph_traversal | semantic_layer (your endpoint's primitives at L0 — usually just direct_lookup is enough) - authority_requiredcanonical | derived | any - on_failurehard_fail | fallback | annotate | abstain - acl_class, pii_class

Worked example: /webgcp/bundles/news-article-retrieval-v0.1.json at the-ai-news. 195 lines of JSON, covers 8 typed Article fields.

Done when: you can write down (in JSON) what shape your endpoint returns, with _meta placeholders.

2.5 Write the query endpoint (§6.3, §6.4)

POST handler. Receives KL-Query JSON, returns typed bundle JSON.

Pseudocode (full reference impl: services/webgcp-query/main.py in the-ai-news's source repo, ~280 lines Python):

@http_post("/webgcp/v0/query")
def query(request):
    # CORS preflight first
    if request.method == "OPTIONS":
        return 204, {"Access-Control-Allow-*": ..., "Access-Control-Allow-Methods": "POST, OPTIONS"}

    body = parse_json(request.body)

    # Validate bundle contract URI
    if body["bundle"]["contract_uri"] != KNOWN_CONTRACT_URI:
        return typed_failure("manifest_mismatch", "...", http_code=409)

    # Validate scope (ACL §5.9)
    if not scope_includes_our_kb(body["scope"]["allow_kbs"]):
        return typed_failure("acl_denied", "...", http_code=403)

    # Validate input (manifest's required inputs)
    if not body.get("filter", {}).get("query"):
        return typed_failure("needs_input", "...", http_code=200, extra={"missing_inputs": ["filter.query"]})

    # Applicability check (§5.4) — out-of-scope returns typed failure, NOT empty results
    scope_check = check_applicability(body["filter"])
    if scope_check:
        return typed_failure("out_of_scope", scope_check["message"], http_code=200,
                             extra={"out_of_scope_reason": scope_check["reason"]})

    # Retrieve
    results = backing_store.fetch(body["filter"])

    # Build typed bundle with _meta on every field (§5.5)
    return {
        "status": "ok",
        "bundle": {
            "articles": {  # or whatever your bundle field is named
                "value": results,
                "_meta": {
                    "source_kb": OUR_KB_URN,
                    "source_uri": MANIFEST_URL,
                    "retrieved_at": iso_now(),
                    "retriever_version": "...",
                    "authority": "derived",
                    "confidence": compute_confidence(results),
                    "manifest_hash": MANIFEST_HASH
                }
            }
        },
        "completeness": "strict_satisfied" if results else "degraded",
        "trace": {"trace_id": body.get("trace", {}).get("trace_id", gen_trace_id()), "spans": [...]},
        "content_origin_summary": {"first_party": len(results), "third_party_trusted": 0, "third_party_untrusted": 0}
    }

Implementing check_applicability() — the reference approach:

At L0, applicability checking is server-side logic that enforces your manifest's out_of_scope_when declarations. The reference implementation uses keyword-matching against declared topic categories:

# Your in-scope topics (from manifest.applicability.in_scope_when[0].values)
IN_SCOPE_TOPICS = {"ai-research", "machine-learning", "llms", "robotics", ...}

# Negative-signal keywords (from manifest.applicability.out_of_scope_when)
OUT_OF_SCOPE_SIGNALS = ["nfl", "playoff", "stock price", "election", "recipe", ...]

def check_applicability(filter_obj):
    """Returns None if in-scope, or {"reason": ..., "message": ...} if out-of-scope."""
    query = filter_obj.get("query", "").lower()
    category = filter_obj.get("category", "")

    # If caller specified a category, check it directly
    if category and category not in IN_SCOPE_TOPICS:
        return {"reason": "category_not_served",
                "message": f"category '{category}' is outside this KB's scope"}

    # Keyword signal: reject queries clearly outside the domain
    for signal in OUT_OF_SCOPE_SIGNALS:
        if signal in query:
            return {"reason": "non_ai_topic",
                    "message": f"query '{query}' is outside this KB's applicability frame"}

    return None  # in-scope — proceed to retrieval

This is deliberately simple. At L0 you don't need an ML classifier or complex predicate engine — a keyword list that catches obviously-wrong queries is sufficient to pass L0-002. The key contract: if your manifest declares something out-of-scope, your endpoint must actually reject it rather than returning empty results.

Typed failures (§6.7) — handle these 5 minimally: - needs_input (HTTP 200) — required input missing - out_of_scope (HTTP 200) — query outside the manifest's applicability frame - manifest_mismatch (HTTP 409) — wrong contract_uri - acl_denied (HTTP 403) — scope.allow_kbs excludes your KB - method_not_allowed (HTTP 405) — non-POST request

Done when: curl -X POST -d '{...}' /webgcp/v0/query returns a JSON body matching §6.4 for valid inputs and matching §6.7 for invalid ones.

2.6 Author the server descriptor (§5.1)

The descriptor is a small JSON document advertising what you serve.

Required fields: - spec_version: "0.1" - conformance_level: "L0" (or "L0-restricted-domain" if you're on .web.app / .github.io / etc.) - server_id: "urn:webgcp:<your-host>" - operator: "<your name>" - endpoints — object with query, manifests, bundle_contracts - manifests_available[] and bundle_contracts_available[] — lists pointing at the artifacts from §2.3 / §2.4 - supported_features: ["bundle_contracts", "direct_lookup"] at L0 - contact: "you@example.com"

If L0-restricted-domain: also add restricted_domain_reason naming the platform.

Worked example: https://the-ai-news.web.app/.well-known/webgcp. 60 lines of JSON.

Done when: curl -sI https://<your-host>/.well-known/webgcp returns HTTP 200 with Content-Type: application/json and an Access-Control-Allow-Origin: * header. CORS open: the descriptor MUST be reachable from any origin.

2.7 Add the DNS SVCB record (§5.1.1)

One record at your registrar:

_webgcp.<your-host>.   3600   IN   SVCB   1   <your-host>.

Priority 1 (ServiceMode), TargetName = your host, no SvcParams. See webgcporg/site/SVCB-AT-SQUARESPACE.md for a Squarespace-specific walkthrough including fallbacks if your registrar's UI doesn't expose SVCB.

Done when:

curl -s -H 'accept: application/dns-json' \
  "https://cloudflare-dns.com/dns-query?name=_webgcp.<your-host>&type=64" | jq .Answer

returns an entry with type: 64 and data starting with 1.

If your host is .web.app / .github.io / .netlify.app / .vercel.app / .pages.dev — skip this step; declare L0-restricted-domain in the descriptor instead. You can come back to it after attaching a custom domain.

2.8 Run the three L0 fixtures

Download and run the conformance runner (zero dependencies — Python 3.8+ stdlib only):

curl -sO https://webgcp.org/conformance/v0.1/runner.py
python3 runner.py --target https://<your-host>/webgcp/v0/query

The runner auto-discovers your server descriptor, manifest, and bundle contract from /.well-known/webgcp, then evaluates all 27 assertions across the three L0 fixtures:

Options:

# If you're on .web.app / .github.io / .netlify.app (no DNS control):
python3 runner.py --target https://<your-host>/webgcp/v0/query --skip-dns

# Provide a known in-scope query for L0-001 (default: auto from manifest):
python3 runner.py --target https://<your-host>/webgcp/v0/query --query "transformers"

# Verbose (show passing assertions too):
python3 runner.py --target https://<your-host>/webgcp/v0/query --verbose

Exit code 0 = all assertions pass. Exit code 1 = at least one failure (printed with assertion ID and spec reference).

Done when: all three fixtures pass (or L0-003 is skipped with --skip-dns and your descriptor declares L0-restricted-domain).

2.9 Publish

Done when: another team can read your /.well-known/webgcp and build to your bundle contract for one of their agents. That's the second-implementer test for your specific surface.


3. Reference implementation walkthrough

The-ai-news at https://the-ai-news.web.app is the live L0-restricted-domain reference. ~280 lines of Python total for the query endpoint; 4 static JSON files; 8 fixture articles bundled in-process.

Source path (in the-ai-news repo) Purpose Approx LOC
services/webgcp-query/main.py The query endpoint — Flask + functions-framework, deploys as Cloud Function Gen2 280
services/webgcp-query/requirements.txt Two deps: functions-framework, flask 2
services/webgcp-query/README.md Deploy + test commands 90
public/.well-known/webgcp Server descriptor (static JSON) 60
public/webgcp/manifests/news-kb-v0.1.json KB manifest 110
public/webgcp/bundles/news-article-retrieval-v0.1.json Bundle contract 180
public/webgcp/conformance/v0.1/WGCP-L0-{001,002,003}.json Fixture copies (downloaded from webgcp.org) 3 × ~150
firebase.json (delta only) Headers for .well-known/webgcp + /webgcp/** paths; rewrite for the query endpoint ~30 added
docs/webGCP-context-integration.md Walkthrough of the integration (Phase 1 + Phase 2) 200

Total new code + config: under 1100 lines. A weekend's work if you've got the host + backing store.

Skim that source to see what each spec MUST looks like in practice. Don't rewrite from scratch — adapt.


4. Deliberately NOT in MVP (out of scope for L0)

These belong to L1+ or to Commerce-layer (separate stack). Skip them.

Concern Where it belongs Skip in L0
Vector search L1 (§4 — "multiple primitives") Yes
Graph traversal L1 / L4 (§5.7) Yes
Hierarchical-tree retrieval L1 (§5.7) Yes
Signed manifests (Cloud KMS, etc.) L2 — Governed (§4) Yes
Right-to-delete L2 (§4) Yes
Federation / cross-server query L3 (§4, §8) Yes
.well-known/webgcp/keys.json trust roots L2+ (§5.1) Yes
Adversarial test suite WGCP-ADV-* L1+ (§9.4) Yes
Payment / x402 / settlement Commerce layer (§0.7 separates this from Context) Yes
Agent marketplace / graph-authority routing Commerce layer Yes
Versioned artifact index L1 (§4) Yes
Refresh SLAs at field level L1 (§4) Yes (manifest-level freshness_sla is enough for L0)
mandatory SvcParam in the SVCB record v0.2 (§5.1.1) Yes

If you find yourself building any of these for L0, stop. They don't gate the standard's L0 conformance and they're better added once your L0 surface is soaked for ≥3 months and you have real friction to design against.


5. Gates — when is L0 "done"?

Hard gates (all must hold to claim L0 in your descriptor):

  1. /.well-known/webgcp returns valid JSON with spec_version: "0.1" and conformance_level: "L0".
  2. DNS SVCB record at _webgcp.<host> resolves with Status: 0 and type: 64. (Or you claim L0-restricted-domain and document why.)
  3. At least one manifest is published and reachable; validates against /schemas/manifest/v0.1.json.
  4. At least one bundle contract is published and reachable; validates against /schemas/bundle/v0.1.json.
  5. Query endpoint accepts a KL-Query, returns a typed bundle with _meta provenance on every value, returns typed failures (not free-form errors) for the 5 failure modes in §2.5.
  6. WGCP-L0-001 passes against the live endpoint (8 positive + 3 negative assertions).
  7. WGCP-L0-002 passes against the live endpoint (4 positive + 3 negative assertions).
  8. WGCP-L0-003 passes against the live DNS (7 positive + 2 negative assertions), OR you legitimately claim L0-restricted-domain.

Soft "should also do": - Listed at /implementations/ on webgcp.org - A docs/webGCP-context-integration.md (or equivalent) in your repo explaining the integration for your team - A way to update your manifest version when content meaningfully changes (no schema yet for the changefeed — that's L4)

When all 8 hard gates hold, you're L0. Update your descriptor with conformance_first_attained_at and start the 3-month soak.


6. Common pitfalls

These are the ones the reference implementation tripped over. Watch for them.

6.1 _meta placement

_meta goes on the bundle field, not on each individual record inside an array. So for a field articles that returns [Article, Article, Article], the shape is:

{
  "bundle": {
    "articles": {
      "value": [{"id": "...", "title": "..."}, ...],
      "_meta": { "source_kb": "...", "authority": "derived", ... }
    }
  }
}

NOT one _meta per Article. If you want per-record provenance for L1+, that's a separate design — but L0 is field-level only.

6.2 Authority class

Search results are derived, not canonical. Even though the underlying titles come from a canonical source, the ranked-results-for-this-query shape is a derived computation. Don't tag a vector-search result set as canonical.

6.3 Out-of-scope is a typed failure, not an empty array

Per §5.4: if the manifest's applicability.out_of_scope_when matches, you MUST return status: "out_of_scope". Never return status: "ok" with bundle.articles.value: [] for an out-of-scope query — that's the "franken-answer" anti-pattern §5.4 specifically forbids.

6.4 URL-honesty

Don't cite URLs in your descriptor or manifest that return 404. If you advertise manifests_available[].url, that URL must serve the manifest right now. Same for bundle_contracts_available[].url. The spec's "URL-honesty" principle (per §5.11 + the v0.1 verification suite) is binary: cited URLs must work or be future-tense.

6.5 CORS

The descriptor, manifest, bundle contract, and conformance fixture files all MUST be served with Access-Control-Allow-Origin: *. They're public protocol surfaces — agents from any origin must be able to fetch them.

6.6 Content-Type

JSON endpoints serve as application/json; charset=utf-8. The descriptor at /.well-known/webgcp has no file extension — explicitly set Content-Type via your hosting platform's header config (Firebase Hosting: firebase.json headers section; nginx: add_header Content-Type ...).

6.7 Typed failures aren't HTTP errors

Per §6.7, most typed failures ride on HTTP 200 with a JSON body whose status field names the failure type. The exceptions: manifest_mismatch → 409, acl_denied → 403, auth_required → 401, kb_unavailable → 503. Don't reflexively map every failure to a 4xx; agents distinguish failure types by the status field, not the HTTP code.

6.8 SVCB priority

SVCB priority 0 is AliasMode (RFC 9460 §2.4) — reserved for v0.2 cross-host delegation. Use priority 1 (ServiceMode) at v0.1. If you accidentally set priority 0, the L0-003 fixture's negative assertion N01 will trip.

6.9 The trailing dot on TargetName

In BIND zone-file format, hostnames end with . to indicate fully-qualified-domain-name. So the SVCB TargetName is webgcp.org. (with dot), not webgcp.org (without). Some registrar UIs strip the trailing dot automatically — that's fine; the wire format is unambiguous either way. But if your DoH check returns data: "1 webgcp.org" (no dot), some strict parsers may complain; prefer the dot when entering at the registrar.


7. Suggested project skeleton

A minimal layout that worked for the reference impl:

your-webgcp-impl/
├── README.md
├── docs/
│   └── webGCP-context-integration.md     # operator-facing notes
├── public/                                # static-published artifacts
│   ├── .well-known/
│   │   └── webgcp                         # server descriptor (JSON, no ext)
│   ├── webgcp/
│   │   ├── manifests/
│   │   │   └── <kb-id>-v0.1.json
│   │   ├── bundles/
│   │   │   └── <contract>-v0.1.json
│   │   └── conformance/
│   │       └── v0.1/
│   │           ├── runner.py              # download from webgcp.org
│   │           ├── WGCP-L0-001.json       # copy from webgcp.org
│   │           ├── WGCP-L0-002.json       # copy from webgcp.org
│   │           └── WGCP-L0-003.json       # copy from webgcp.org
│   └── (rest of your site)
├── services/
│   └── webgcp-query/
│       ├── main.py                        # ~250-300 lines for L0
│       ├── requirements.txt
│       └── README.md                      # deploy commands
└── firebase.json (or nginx.conf, k8s yamls, etc.)

Adapt to your stack. The names of public/, services/, etc. don't matter — what matters is the URL paths the published artifacts end up at.


8. Tech choices that worked

These are one path through L0, not the only path. Substitute freely.

Concern Reference impl used Acceptable substitutes
Hosting (static) Firebase Hosting Netlify, Vercel, Cloudflare Pages, GCS+CDN, S3+CloudFront, GitHub Pages (restricted-domain), self-hosted nginx
Hosting (dynamic) Cloud Function Gen2 (Python) Cloud Run, AWS Lambda, Vercel Functions, Cloudflare Workers, self-hosted FastAPI/Flask/Express, k8s Pod
Language Python 3.11 TypeScript, Go, Rust, Java, Elixir — anything that handles HTTP POST + JSON
Backing store In-process fixture dict Firestore, Postgres+pgvector, BigQuery, Snowflake, DynamoDB, SQLite, files on disk
DNS Squarespace (TXT/A) + Cloudflare migration (SVCB) Any DNS provider with SVCB support: Cloudflare, Google Cloud DNS, AWS Route 53, NSD/Knot/BIND9 self-hosted
Build Angular ng build (since the site embeds in an existing Angular app) Pure static (no build), Eleventy, Astro, Next.js, Hugo

Cheapest-to-ship combo: Cloudflare Pages (free) + Cloudflare Workers (free L0) + Cloudflare DNS (free SVCB). Likely under 100 lines of TypeScript for the Worker.


9. Timeline + effort

For a competent engineer with the host + backing-store decision already made:

Step Effort
§2.3 Manifest (one KB) 1–2 hours
§2.4 Bundle contract (one type) 1–2 hours
§2.5 Query endpoint 4–8 hours (most of the work)
§2.6 Server descriptor 30 min
§2.7 DNS SVCB 5 min in UI; up to 30 min if migrating providers
§2.8 Run fixtures 30 min
§2.9 Publish + DNS propagation 30 min – 24 h (mostly waiting)

Total active engineering: 8–16 hours. Plus DNS propagation latency. A focused weekend gets you to L0.

If you don't have a backing store yet, add 1–2 days to wire something up (or just bundle fixture data for the demo — the-ai-news did this).

If you don't control a domain, add another day for registrar + DNS setup. (Or use a .web.app / similar and accept L0-restricted-domain.)


10. After L0 ships

The standards-playbook clock starts (per establishing-a-standard.md §1, Step 3):

L1 work — vector search, multiple primitives, versioned artifacts — is rational only after L0 has soaked. Don't pre-build L1 features into L0.


11. Cross-references


12. Filing issues + contact

No working group is convened as of 2026-05-16; v0.1 governance is single-author transparency (see /governance/).

To file issues, propose changes, or signal you've shipped: - Email the contact in https://webgcp.org/.well-known/webgcp (operator + contact fields) - Once a working group convenes and a GitHub org exists, that becomes the canonical issue tracker (currently pending — see spec §11.3)


Last updated: 2026-05-16. Authored alongside the v0.1 spec publication.