X webhook ingress #
Boundary #
The X connector is an ingress-only producer for the current X Activity API and v2 Webhooks API. It verifies X delivery, normalizes registered activity events, and appends them to Jazz. It cannot invoke a model, send an X action, register its own webhook, change subscriptions, start a replay, or refresh a user grant.
The connector does not implement the deprecated Account Activity API. Public-reply coverage through Filtered Stream is also outside the first implementation: X documents to: rules for replies, but its webhook delivery product is currently Enterprise-only. A later pay-per-use persistent-stream connector may cover that gap without weakening this webhook contract.
Source lanes #
One ingress service may host several exact loopback routes. Each route owns one source id, one public webhook registration, one source-local sequence, and one privacy lane. The HTTP path selects the lane before body parsing; event content cannot select or widen its destination.
The first deployment uses the following lanes:
| Lane | Initial subscriptions | Authentication | Event privacy |
|---|---|---|---|
| Personal public | post.create, post.delete for the configured owner user id |
App-only bearer for management | public-source |
| Personal private | Outbound like.create for the configured owner user id |
Separate user-context OAuth grant with exact scopes | sensitive |
| Public watchlist | post.create, post.delete for explicit watched user ids |
App-only bearer for management | public-source |
The connector supports the personal-private outbound-like contract, but repository configuration leaves it disabled. Activation requires a separately provisioned user-context access token, exact outbound subscription direction, a credential compartment distinct from app-only management, and an operator-owned refresh/expiry procedure. Static source code or a token environment reference is not an OAuth receipt. Profile, Spaces, inbound likes, follows, mentions, DMs/chat, mutes, blocks, revocation, and other private events remain outside this contract.
X Activity post.mention.create does not prove complete reply coverage. A reply may instead require a Filtered Stream to:<handle> rule. The system must describe coverage as the exact enabled event/rule set rather than “everything involving this account.”
Manifest contract #
Each x-webhook source declares:
- a stable source id and explicit
enabledstate; - a lane:
personal-public,personal-private, orpublic-watchlist; consumerSecretEnvfor CRC and POST signature verification;managementBearerTokenEnvfor explicit operator commands only;- an HTTPS
webhookUrl, matching normalizedwebhookPath, and loopback host/port; - bounded request bytes and an acknowledgement deadline below X's 10-second limit;
- one or more expected subscriptions with exact event type, numeric user id, optional exact direction, nonsecret tag, privacy, and eventual webhook id;
- a connector revision included in deterministic identity.
Expected subscriptions may be written inline or materialized from one project-relative subscriptionFile. The file form remains public-post-only strict YAML containing one matching source id, one or both admitted post event types, and a bounded account list of exact handle plus numeric userId pairs. Direction-bound private likes use inline subscriptions so the direction is visible in the owning source declaration. The handle is the operator-facing label; the stable user id remains the admission and subscription identity. Manifest loading refuses path escape, symlinks, source mismatch, duplicate case-insensitive handles, duplicate user ids, duplicate event types, oversized files, and a source that declares both forms. It deterministically expands every account/event pair into a source-owned tag before ordinary manifest validation, so receiver and management processes consume the same exact desired subscriptions without network resolution at startup.
The public URL has no credentials, query, fragment, or explicit port. Subscription tags are stable protocol labels, not prose. The receiver process receives only the consumer secret. Operator commands receive the bearer token in a separate credential compartment. No model consumer receives either credential. X_CONSUMER_KEY is not required by the public-event receiver and is not added merely because it exists.
An x-webhook source fails startup when two routes share a public path, source id, or expected subscription tuple; when an event type or direction conflicts with its lane; when a personal-private route lacks user-context management authority; or when a public route names an event that X documents as user-context-only. The only admitted private tuple is like.create with direction: outbound.
CRC and POST admission #
The receiver handles two request forms on each exact path:
- A CRC
GETcontains one boundedcrc_tokenquery value and may contain one boundednoncevalue. X's current registration service emits the nonce even though the public quickstart documents onlycrc_token; it is admitted solely as a cache-busting transport field and is not part of the HMAC. Any other key or duplicate fails closed. The receiver returns JSON containingresponse_token = sha256=<base64 HMAC-SHA256>, computed over the CRC token with the app consumer secret. The CRC path does not open Jazz. - An event
POSTrequires JSON and onex-twitter-webhooks-signatureheader. The receiver buffers bounded raw bytes, computes Base64 HMAC-SHA256 over those exact bytes with the consumer secret, prependssha256=, and compares the complete value in constant time before parsing JSON or opening Jazz.
Wrong path, method, query shape, media type, length, signature multiplicity, or signature value fails before persistence. The receiver parses the common X Activity envelope, then requires an exact configured event type, filter user id, and subscription tag for the selected route. A valid X signature does not admit an undeclared subscription.
Requests on one route are serialized through a bounded durable append lane. A full queue rejects new work with retryable 5xx rather than accumulating unbounded requests. The receiver returns 200 only after the normalized source event and terminal connector-ingest evidence are durable. Store failure or local deadline expiry also returns 5xx; X may retry, and deterministic identity absorbs a late-success/retry overlap. The receiver never logs or persists request headers, signatures, secrets, bearer values, raw bodies, or parsed fields outside the registered normalized payload.
Event contract #
Every admitted activity produces stream.thought.source.x.activity@1 with:
- X
event_uuidasexternalIdand retainedeventUuid; - exact normalized
eventType, matched user id, subscription tag, and lane; - an event-specific strict payload;
- connector revision and signature-verified transport classification. The receipt-body SHA-256 stays on content-dark delivery lifecycle events so semantically identical X replays do not diverge merely because ignored metrics, expansions, or JSON serialization changed;
occurredAtfrom an event-specific immutable timestamp when one exists, otherwise receipt time;- the route's source id, source-local sequence, privacy, and ordinary thought stream lineage.
V1 post.create retains the stable post id, author id, text, creation time, conversation id, edit-history ids, reply/reference ids, language, entities, and bounded media references that X delivered. It excludes mutable public metrics and mutable user-profile expansions so a replay of one activity cannot conflict merely because counters or profile text changed. It stores attachment references only and never downloads media. post.delete retains the deleted post id and author id.
V1 private like.create is admitted only when the signed envelope, configured subscription, matched owner user id, and outbound direction all agree. It retains the stable like-event id, liking user id, liked post id, liked-post author id, optional liked-post creation time, and optional event timestamp. The action timestamp comes from timestamp_ms; created_at describes the liked post and must not be mislabeled as the like time. Mutable includes, metrics, and profile expansions are discarded. The event and its connector lifecycle evidence are sensitive.
The idempotency key binds source id, connector revision, and event_uuid. X explicitly warns that webhook delivery may duplicate events and recommends event-id deduplication. Exact replay addresses the same row; divergent normalized content under the same key fails closed. Delivery attempts may still produce separate content-dark ingest receipts, so retries remain observable without creating duplicate source activity.
Unknown event types, missing stable identifiers, wrong users or tags, invalid timestamps, oversized text/entities, and payloads that cannot satisfy the registered event-specific schema produce no source event. Diagnostics retain only an allowlisted classification, body hash, source id, event-type presence, and counts.
Management authority #
Webhook and subscription mutation remains outside the receiver. Operator commands provide the following surfaces:
x-webhook-status: read webhook validity and current subscriptions;x-webhook-register: create one configured webhook, pass CRC, and read back exact id/URL/validity;x-subscriptions-plan: compare desired and live subscriptions without mutation;x-subscriptions-apply: apply one displayed bounded diff and read it back;x-webhook-replay: request an explicit bounded replay no older than X's 24-hour limit;x-webhook-delete: remove one exact registration only after readback proves no live subscription still references it.
Startup never calls these commands. A credential appearing in the environment is not registration or subscription authority. Activation first proves the receiver at its final public URL, then registers the webhook, then applies subscriptions. Every mutation prints a content-dark verification receipt containing source identity, webhook/subscription ids or counts, the binding URL or plan hash, and the final readback result. It contains no credential, request body, provider response body, or account content. Persisting operator output is deployment custody, not a hidden connector side effect.
Before activation, the operator separately verifies that the app's current package exposes every requested endpoint, confirms the current external rates, and sets a provider-side spending limit because X pricing is mutable and event rejection cannot undo a delivery charge. The status command does not pretend webhook/subscription readback proves pricing or package entitlement. Prices are not hardcoded into the connector.
Activation order #
Activation is a separate operator change, not part of implementation or process startup:
- Replace tracked example ids and URLs in a private manifest while keeping the X sources, batch, and consumer disabled.
- Materialize
x-webhook.envandx-management.env; verify their variable-name receipts without printing values. - Confirm the current X package, endpoint entitlement, unit rates, and provider-side spending limit.
- Start the loopback receiver behind the exact HTTPS reverse-proxy path and prove CRC locally before registration.
- Run read-only status, register only with its returned URL hash, then run subscription plan and apply only with the unchanged plan hash.
- Verify one signed public fixture and one natural public delivery reach Jazz exactly once before enabling any model consumer.
- Provision the output-only X listener under its own agent identity, place that id plus its explicit activation gate in the consumer compartment, and enable
cameron-x-activity-windowwith the declaration only after natural ingress is proven. - Enable the social-observation batch and social listener only after both source-local listeners have completed natural runs with exact durable outputs. A live Letta run, private-event OAuth grant, account subscription expansion, replay, webhook deletion, deploy, or service restart each remains a separate explicit action.
Recovery and health #
Webhook delivery has no cursor. The source-local sequence records durable receipt order but does not claim upstream order. Earlier timestamps and replayed events remain admissible.
The receiver acknowledges only durable events, while event identity absorbs ordinary retries. X exposes replay for events delivered or attempted during at most the preceding 24 hours. Replay is an explicit operator action over an exact UTC interval and webhook id; the connector never infers a replay interval or starts one after a restart.
A credentialed read-only health process compares webhook validity, URL, and live subscriptions with the manifest's desired state. It updates content-dark source-control state containing only registration validity, desired/live counts, convergence, and check time; it never stores credentials, callback URLs, account content, or repairs drift. When an independent service-manager precondition proves the receiver process active, the health process also renews its readiness receipt. The inspector treats readiness and upstream checks as bounded leases, then merges them with durable event/cursor evidence rather than inferring source existence from activity. Outage beyond 24 hours is a named data-loss boundary. A later REST reconciler may recover surviving public posts, but it cannot prove deleted-post completeness and cannot relabel a partial backfill as lossless recovery.
Downstream batching and models #
No X source activates a consumer. Personal and public-watchlist lanes use separate source ids so a declaration cannot gain private activity by subscribing to the public watchlist.
Public-watch, Cameron-public, and Cameron-private activity keep separate source identities and credential boundaries. Their admitted source events may join both the resident Stream's mixed-source ten-minute activity window and the disabled source-local X window. Each batch retains every member's source identity and complete per-source prefix, then takes the most-private member's privacy class. It never merges the ingress sources themselves or widens a public subscription into private activity.
The X listener emits private derived observations under its own agent source. A separate social batch may combine those observations with completed Bluesky-listener observations, but its trusted context compiler verifies exact completed-run lineage before exposing any member to the social model. Social synthesis therefore does not inherit raw X records or X credentials. Only the social listener's exact batch route may propose Telegram attention; neither X ingress nor the X listener can deliver.
Model invocation is never one call per webhook event by default. Provider event spend, inference usage, batch compression, and accepted outputs remain separate receipts. A cheap model route does not justify widening the watched-account list or private-event scope.
Acceptance gates #
The connector remains disabled until the following gates pass:
- Captured synthetic fixtures cover CRC, signatures over raw bytes, every admitted V1 event, duplicate and divergent replay, wrong route/user/tag/type, malformed and oversized bodies, out-of-order delivery, deadline expiry, and durable append failure without network access.
- Credential-compartment tests prove the receiver sees only the synthetic consumer secret, operator commands see only the synthetic bearer token and required management values, and consumers see neither.
- A loopback canary proves
200follows durable append and5xxfollows store failure. It uses invented users, posts, tags, and secrets. - Before mutation, the operator produces a private activation record for package access, webhook quota, current pricing, current subscriptions, and the configured provider spending limit. The connector does not claim to infer these from status readback.
- Fresh authorization separately enables webhook registration, subscription mutation, and one natural delivery canary. No model consumer is part of the ingress acceptance gate.