@volter/twin-x 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +138 -0
  3. package/dist/client/x-mirror.bundle.js +321 -0
  4. package/dist/client/x-mirror.d.ts +45 -0
  5. package/dist/client/x-mirror.js +417 -0
  6. package/dist/src/cli.d.ts +2 -0
  7. package/dist/src/cli.js +29 -0
  8. package/dist/src/index.d.ts +14 -0
  9. package/dist/src/index.js +68 -0
  10. package/dist/src/x-budget.d.ts +54 -0
  11. package/dist/src/x-budget.js +123 -0
  12. package/dist/src/x-capabilities.d.ts +3 -0
  13. package/dist/src/x-capabilities.js +1106 -0
  14. package/dist/src/x-conformance.d.ts +8 -0
  15. package/dist/src/x-conformance.js +91 -0
  16. package/dist/src/x-connector.d.ts +125 -0
  17. package/dist/src/x-connector.js +546 -0
  18. package/dist/src/x-media.d.ts +87 -0
  19. package/dist/src/x-media.js +275 -0
  20. package/dist/src/x-mirror-ui.d.ts +61 -0
  21. package/dist/src/x-mirror-ui.js +253 -0
  22. package/dist/src/x-problems.d.ts +38 -0
  23. package/dist/src/x-problems.js +130 -0
  24. package/dist/src/x-scopes.d.ts +7 -0
  25. package/dist/src/x-scopes.js +62 -0
  26. package/dist/src/x-server.d.ts +14 -0
  27. package/dist/src/x-server.js +127 -0
  28. package/dist/src/x-twin.d.ts +21 -0
  29. package/dist/src/x-twin.js +1534 -0
  30. package/package.json +58 -0
  31. package/src/cli.ts +27 -0
  32. package/src/index.ts +132 -0
  33. package/src/x-budget.ts +150 -0
  34. package/src/x-capabilities.ts +1161 -0
  35. package/src/x-conformance.ts +113 -0
  36. package/src/x-connector.ts +546 -0
  37. package/src/x-media.ts +295 -0
  38. package/src/x-mirror-ui.ts +263 -0
  39. package/src/x-problems.ts +143 -0
  40. package/src/x-scopes.ts +67 -0
  41. package/src/x-server.ts +126 -0
  42. package/src/x-twin.ts +1545 -0
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@volter/twin-x",
3
+ "version": "0.1.0",
4
+ "description": "Local X (Twitter) API v2 twin (post, reply, delete, timelines, user lookup) with an x.com mirror, built on @volter/world-core.",
5
+ "author": "Volter (https://github.com/volter-ai)",
6
+ "license": "Apache-2.0",
7
+ "files": [
8
+ "src",
9
+ "README.md",
10
+ "LICENSE",
11
+ "!**/*.test.ts",
12
+ "!**/*.test.tsx",
13
+ "dist"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/volter-ai/twin.git",
18
+ "directory": "packages/twin/x"
19
+ },
20
+ "homepage": "https://github.com/volter-ai/twin/tree/main/packages/twin/x#readme",
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/src/index.d.ts",
25
+ "default": "./dist/src/index.js"
26
+ }
27
+ },
28
+ "bin": {
29
+ "world-x": "dist/src/cli.js"
30
+ },
31
+ "scripts": {
32
+ "test": "bun test src/*.test.ts",
33
+ "typecheck": "tsc --noEmit",
34
+ "build": "node ../../../scripts/publish/build.mjs",
35
+ "prepack": "node ../../../scripts/publish/prepare-publish.mjs prepack",
36
+ "postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
37
+ },
38
+ "peerDependencies": {
39
+ "@volter/world-core": "2.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/bun": "^1.2.20",
43
+ "@types/node": "^24.0.0",
44
+ "@types/react": "^19.2.17",
45
+ "@types/react-dom": "^19.2.3",
46
+ "@volter/world-core": "2.0.0",
47
+ "@volter/world-tooling": "0.1.0",
48
+ "twitter-api-v2": "^1.29.1",
49
+ "typescript": "^5.9.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=22.3"
53
+ },
54
+ "dependencies": {
55
+ "react": "^19.2.7",
56
+ "react-dom": "^19.2.7"
57
+ }
58
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { keepProcessAlive } from '@volter/world-core/lifecycle';
3
+ import { hasFlag, optionValue } from '@volter/world-core/args';
4
+ import { createXTwinServer } from './x-server.ts';
5
+ import { createXMirrorServer } from './x-mirror-ui.ts';
6
+
7
+ const [cmd, ...rest] = process.argv.slice(2);
8
+ const port = Number(optionValue(rest, '--port', '0')) || undefined;
9
+ const root = optionValue(rest, '--root') || undefined;
10
+ const readOnly = hasFlag(rest, '--read-only');
11
+
12
+ if (cmd === 'serve') {
13
+ const server = await createXTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) });
14
+ process.stdout.write(`x twin${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:${server.port}\n`);
15
+ await keepProcessAlive();
16
+ } else if (cmd === 'mirror') {
17
+ const s = await createXMirrorServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) });
18
+ process.stdout.write(`x mirror UI (x.com: sign in, profile, timeline, post, compose) at ${s.url}\n`);
19
+ await keepProcessAlive();
20
+ } else if (cmd === 'conformance') {
21
+ const { checkXConformance } = await import('./x-conformance.ts');
22
+ const report = await checkXConformance({ ...(root ? { root } : {}) });
23
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
24
+ if (!report.ok) process.exitCode = 1;
25
+ } else {
26
+ process.stdout.write('Usage: world-x serve|mirror|conformance [--port N] [--root DIR] [--read-only]\n');
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,132 @@
1
+ export { handleXTwinRequest } from './x-twin.ts';
2
+ export type { XRequest } from './x-twin.ts';
3
+ export { createXTwinFetch, createXTwinServer, type XTwinFetchOptions } from './x-server.ts';
4
+ export {
5
+ budgetedXExecute,
6
+ liveXExecute,
7
+ MAX_PROCESSING_POLLS,
8
+ MAX_PROCESSING_WAIT_MS,
9
+ XVideoStillProcessingError,
10
+ mapTimelinePost,
11
+ pullXMentions,
12
+ pullXOwnTimeline,
13
+ pushPendingXActions,
14
+ pushXAction,
15
+ performXAction,
16
+ PULL_TWEET_FIELDS,
17
+ syncXFromReal,
18
+ xRequestForAction,
19
+ } from './x-connector.ts';
20
+ export type { LiveXOptions, XExecute } from './x-connector.ts';
21
+ export {
22
+ forbiddenProblem,
23
+ invalidRequestProblem,
24
+ notFoundProblem,
25
+ rateLimitExceeded,
26
+ readOnlyRefusal,
27
+ resourceNotFoundProblem,
28
+ unauthorizedProblem,
29
+ } from './x-problems.ts';
30
+ export type { XResponse } from './x-problems.ts';
31
+ export {
32
+ avatarHue,
33
+ avatarInitial,
34
+ buildXMirrorClient,
35
+ createXMirrorServer,
36
+ displayUrl,
37
+ durationLabel,
38
+ fullTimestamp,
39
+ joinedLabel,
40
+ mediaByKey,
41
+ postMedia,
42
+ PROFILE_READ_QUERY,
43
+ POST_READ_QUERY,
44
+ referencedId,
45
+ relativeTime,
46
+ textSegments,
47
+ tweetsById,
48
+ usersById,
49
+ videoSource,
50
+ xMirrorHtml,
51
+ xMirrorStyles,
52
+ } from './x-mirror-ui.ts';
53
+ export type { TextSegment, XRow } from './x-mirror-ui.ts';
54
+ export { missingScopes, parseScopeList, REQUIRED_SCOPES, X_SCOPES } from './x-scopes.ts';
55
+ // The client-side rate budget — the fail-closed backstop `liveXExecute` routes every live
56
+ // request through. The mechanism is the kernel's (`@volter/world-core` -> rateBudget.ts); these are
57
+ // this vendor's numbers and the typed bindings around them.
58
+ export {
59
+ X_BUDGET_BURST_CEILING,
60
+ X_BUDGET_CEILING,
61
+ X_BUDGET_MAX_RETRY_AFTER_S,
62
+ X_BUDGET_WINDOW_MS,
63
+ X_CALL_WEIGHTS,
64
+ X_RATE_BUDGET,
65
+ XBudget,
66
+ XBudgetError,
67
+ xBudgetPath,
68
+ xCallWeight,
69
+ } from './x-budget.ts';
70
+ export type { XBudgetErrorKind, XBudgetOptions, XBudgetReservation, XBudgetSnapshot } from './x-budget.ts';
71
+
72
+ import type { TwinPack } from '@volter/world-core';
73
+ import { X_RATE_BUDGET as RATE_BUDGET } from './x-budget.ts';
74
+
75
+ /**
76
+ * The api.x.com / api.twitter.com paths THIS pack serves, as a RegExp SOURCE for the descriptor's
77
+ * `hosts` path rule (the injector compiles it with `new RegExp` and tests it against the pathname).
78
+ * The POSTING slice — create/quote/reply (one route), retract, post lookup (single and bulk),
79
+ * recent search, the three per-user timelines (mentions, the account's own posts, the home
80
+ * timeline), user lookup by id and by username, and image upload (one-shot and chunked) — plus the
81
+ * twin-only control prefix. The user-id
82
+ * rule EXCLUDES `/2/users/me`: that route is xidentity's, and the two claims stay disjoint. ANCHORED: an unanchored claim would swallow
83
+ * `/2/tweetsomething`. Everything else on api.x.com stays unclaimed, so an unmodelled X call
84
+ * refuses loudly rather than landing in a twin that cannot serve it.
85
+ */
86
+ const X_API_PATHS = '^(?:/2/media/upload/?|/2/media/upload/initialize/?|/2/media/upload/[0-9]+/(?:append|finalize)/?|/2/tweets/?|/2/tweets/[^/]+/?|/2/tweets/search/recent/?|/2/users/(?!me/?$)[^/]+/?|/2/users/by/username/[^/]+/?|/2/users/[^/]+/mentions/?|/2/users/[^/]+/tweets/?|/2/users/[^/]+/timelines/reverse_chronological/?)$|^/_twin/';
87
+
88
+ export const pack: TwinPack = {
89
+ vendor: 'x',
90
+ // The SAME object x-budget.ts declares at module load — one source of truth, so registering
91
+ // the pack and importing the connector arm identical numbers.
92
+ rateBudget: RATE_BUDGET,
93
+ transport: 'rest',
94
+ archetype: 'crud',
95
+ bin: 'world-x',
96
+ resources: ['account', 'token', 'post', 'follow', 'rate_limit'],
97
+ specSource: 'X API v2 posting surface — POST /2/tweets (original post, QUOTE and reply, told apart by quote_tweet_id / reply.in_reply_to_tweet_id), DELETE /2/tweets/:id, GET /2/tweets/:id and GET /2/tweets?ids=, GET /2/tweets/search/recent, GET /2/users/:id/{mentions,tweets,timelines/reverse_chronological}, GET /2/users/:id and GET /2/users/by/username/:username, with tweet.fields/user.fields/expansions projection and next_token paging. Routes and body shapes grounded in the pinned twitter-api-v2@1.29.1 client source; error envelopes and the scope catalog transcribed from packages/twin/xidentity (X API v2 OpenAPI 2.167). See test-fixtures/x-openapi-operations.SOURCE.md.',
98
+ description: "X twin — the org's public voice: post, quote, reply to a mention, retract, look a post or an account back up, and read the mentions / own / home timelines and recent search, offline, with X's own tweet.fields projection, bearer/scope gate and problem envelopes. OAuth itself is xidentity's and stays there.",
99
+ // Adoption + interception, moved off the central maps unchanged (descriptor-first back-migration, adding-a-twin.md §3,
100
+ // 2026-08-31).
101
+ //
102
+ // `twitter-api-v2` is claimed by THIS pack, not by `xidentity`: it is a third-party library
103
+ // spanning the whole X API (posts, streams, DMs), and this pack models the POSTING surface it is
104
+ // actually installed for. No env stem is claimed — X_CLIENT_ID / X_CLIENT_SECRET and
105
+ // TWITTER_CLIENT_ID name the OAuth APP credentials, which is xidentity's surface; a repo that
106
+ // posts holds a user access token that flow minted, so the app credential and the posting client
107
+ // are the same integration seen from two sides and both twins report as covered.
108
+ adoption: {
109
+ // COMMUNITY: X ships no Python SDK (its official clients are `@xdevplatform/xdk` and the legacy
110
+ // `twitter-api-sdk`, both TypeScript). `tweepy` is the de-facto Python client of the X API and is
111
+ // claimed HERE rather than on `xidentity` because its dominant use is exactly this pack's surface
112
+ // - posting and reading tweets/mentions - not the OAuth token exchange. One name, one pack.
113
+ pypi: ['tweepy'],
114
+ sdks: ['twitter-api-v2'],
115
+ },
116
+ // The POSTING surface — the org's public voice. Path-scoped for the same reason xidentity is:
117
+ // api.x.com is the whole X API v2 host and this pack models one slice. The two packs' path sets
118
+ // are DISJOINT BY CONSTRUCTION: `xidentity` claims `/2/oauth2/{token,revoke}` and `/2/users/me`;
119
+ // this claims `/2/tweets*`, the per-user timelines, `/2/users/{id}` and
120
+ // `/2/users/by/username/{username}`. The user-id rule carries a `(?!me/?$)` lookahead and the
121
+ // timeline rules need a trailing segment, so `/2/users/me` matches none of them and no request
122
+ // is claimable by both. ANCHORED, the
123
+ // googleoauth lesson: an unanchored claim swallows `/2/tweetsomething`. Everything else on
124
+ // api.x.com — DMs, spaces, lists, media metadata — stays unclaimed, so an unmodelled X call
125
+ // refuses loudly rather than landing in a twin that cannot serve it. api.twitter.com is claimed
126
+ // alongside api.x.com because that is the host X's own legacy clients still target.
127
+ hosts: [
128
+ { host: 'api.x.com', pathPattern: X_API_PATHS },
129
+ { host: 'api.twitter.com', pathPattern: X_API_PATHS },
130
+ ],
131
+ browserRouting: { apiPathPrefix: '/2/', loaderHost: 'https://api.x.com' },
132
+ };
@@ -0,0 +1,150 @@
1
+ // The X posting surface's CLIENT-SIDE RATE BUDGET — this pack's DECLARATION (the numbers) plus
2
+ // the thin typed bindings `liveXExecute` uses. The MECHANISM — the durable token-keyed ledger,
3
+ // the rolling window, reserve-under-lock, the `Retry-After`/429 cooldown, fail-CLOSED on a
4
+ // corrupt ledger — lives ONCE in the vendor-agnostic kernel (`@volter/world-core` → rateBudget.ts).
5
+ //
6
+ // ── HOW THE CEILING WAS CHOSEN ──────────────────────────────────────────────────────────────
7
+ // The vendor's SCHEME is first-party and recorded in this repo: X publishes per-endpoint rate
8
+ // limits on 15-minute windows and answers an exceeded window with HTTP 429 and legacy error
9
+ // code 88 (docs.x.com/x-api/fundamentals/rate-limits, fetched 2026-08-21 — the fetch recorded in
10
+ // spec-sources.json's `xidentity` entry, and transcribed by
11
+ // packages/twin/xidentity/src/xidentity-budget.ts). This declaration transcribes the SCHEME: the
12
+ // window is the vendor's own 15 minutes, and 429/code-88 is what the guard expects back.
13
+ //
14
+ // EVIDENCE BOUNDARY — READ THIS BEFORE TRUSTING THE NUMBER. The per-endpoint FIGURES for the
15
+ // POSTING endpoints (`POST /2/tweets`, `DELETE /2/tweets/:id`, `GET /2/users/:id/mentions`) were
16
+ // NOT fetched by this build: no X call is made anywhere in this pack, by standing rule, and the
17
+ // figure recorded in this repo covers `GET /2/users/me` only. So the ceiling here is NOT a
18
+ // transcription — it is a deliberately CONSERVATIVE choice: 50 weighted units per 15 minutes,
19
+ // BELOW every posting figure X is reported to publish, with a WRITE priced at 5 and a read at 1
20
+ // so a runaway poster exhausts the budget an order of magnitude before a runaway reader does.
21
+ // Pinning these to the vendor's published figures is `x.rate_limit.published_figures` (todo).
22
+ // A budget that is too tight fails closed and costs a retry; one that is too loose spends a real
23
+ // org's real posting allowance, so the asymmetry decides the direction.
24
+ import {
25
+ declareRateBudget,
26
+ rateBudgetPath,
27
+ rateBudgetWeight,
28
+ RateBudget,
29
+ type RateBudgetDeclaration,
30
+ type RateBudgetOptions,
31
+ type RateBudgetReservation,
32
+ type RateBudgetSnapshot,
33
+ } from '@volter/world-core';
34
+
35
+ const VENDOR = 'x';
36
+
37
+ /** Rolling window, in ms — X's own 15-minute rate-limit window. */
38
+ export const X_BUDGET_WINDOW_MS = 15 * 60_000;
39
+
40
+ /** Weighted units allowed inside one window. See the EVIDENCE BOUNDARY above: conservative, not
41
+ * transcribed. */
42
+ export const X_BUDGET_CEILING = 50;
43
+
44
+ /** Weighted units allowed in any 60s (the kernel's burst sub-window): 10, well under the kernel
45
+ * fallback's burst of 30, so no VENDOR_BURST_ANCHOR entry is required in
46
+ * scripts/rate-budget-isolation.test.ts. */
47
+ export const X_BUDGET_BURST_CEILING = 10;
48
+
49
+ /** Seconds. A `Retry-After` above this means the client is throttled hard — fail loudly, don't sleep. */
50
+ export const X_BUDGET_MAX_RETRY_AFTER_S = 900;
51
+
52
+ /** Per-call cost, keyed by `"<METHOD> <path>"`. */
53
+ export const X_CALL_WEIGHTS = {
54
+ /** A post or a reply — the org's public voice SPEAKING. The expensive act. */
55
+ write: 5,
56
+ /** Reading who mentioned the org. Cheap: it is the input to a decision, not an act. */
57
+ read: 1,
58
+ /** A media upload call (one-shot, initialize, append, finalize) and its STATUS read. Staging a
59
+ * file publishes nothing — only the post that attaches it speaks — so it is priced as a read:
60
+ * the smallest weight the kernel allows, the post itself still paying the write. */
61
+ media: 1,
62
+ /** Everything else on the vendor: no figure of our own → priced at the write. */
63
+ other: 5,
64
+ } as const;
65
+
66
+ /** THE PACK'S DECLARATION — pure data, the only X-specific thing in the whole budget. */
67
+ export const X_RATE_BUDGET: RateBudgetDeclaration = {
68
+ windowMs: X_BUDGET_WINDOW_MS,
69
+ ceiling: X_BUDGET_CEILING,
70
+ burstCeiling: X_BUDGET_BURST_CEILING,
71
+ defaultWeight: X_CALL_WEIGHTS.other,
72
+ maxRetryAfterSeconds: X_BUDGET_MAX_RETRY_AFTER_S,
73
+ rules: [
74
+ { match: '^POST /2/tweets$', weight: X_CALL_WEIGHTS.write },
75
+ { match: '^DELETE /2/tweets/', weight: X_CALL_WEIGHTS.write },
76
+ { match: '^GET /2/users/[^/]+/mentions$', weight: X_CALL_WEIGHTS.read },
77
+ { match: '^POST /2/media/upload(?:/initialize|/[0-9]+/(?:append|finalize))?$', weight: X_CALL_WEIGHTS.media },
78
+ { match: '^GET /2/media/upload$', weight: X_CALL_WEIGHTS.media },
79
+ ],
80
+ reason:
81
+ "X publishes per-endpoint rate limits on 15-minute windows and answers an exceeded window with "
82
+ + 'HTTP 429 and legacy error code 88 (docs.x.com/x-api/fundamentals/rate-limits, fetched '
83
+ + '2026-08-21 and recorded in spec-sources.json under xidentity). The WINDOW here is the '
84
+ + "vendor's own 15 minutes and the 429/code-88 pair is what the guard expects back. The "
85
+ + 'per-endpoint FIGURES for the posting endpoints were NOT fetched by this build — no X call is '
86
+ + 'made anywhere in this pack, and the figure this repo holds covers GET /2/users/me only — so '
87
+ + 'the ceiling of 50 units is a deliberately CONSERVATIVE choice rather than a transcription, '
88
+ + 'sitting below every posting figure X is reported to publish. A write (a post or a reply — the '
89
+ + "org's public voice speaking, on a real account, in public) costs 5; reading the mentions "
90
+ + 'timeline costs 1, because reading is the input to a decision and posting is the act. So a '
91
+ + 'runaway poster exhausts the window after ten posts while a runaway reader gets fifty reads. '
92
+ + 'A media upload call (one-shot, initialize, append, finalize, STATUS) costs 1: staging a file '
93
+ + 'publishes nothing, and the post that attaches it still pays the write. '
94
+ + 'Pinning these to the vendor\'s published posting figures is x.rate_limit.published_figures '
95
+ + '(todo). A budget that is too tight fails closed and costs a retry; one that is too loose '
96
+ + "spends a real org's real posting allowance in public, so the asymmetry decides the direction. "
97
+ + "The burstCeiling of 10 units/60s bounds the instant and stays under the kernel fallback's 30.",
98
+ };
99
+
100
+ // Declared at module load, so merely importing this module (which `x-connector.ts` does) is
101
+ // enough to arm the real ceiling.
102
+ declareRateBudget(VENDOR, X_RATE_BUDGET);
103
+
104
+ /**
105
+ * Price one call. The key is `"<METHOD> <path>"` with the query string split off. NORMALIZED
106
+ * (upper-cased method, collapsed trailing slash) because the anchored rules are otherwise
107
+ * trivially evaded by input variation: `fetch` upper-cases a known method before sending, so
108
+ * `execute('post', '/2/tweets')` really does post in public and must be priced as one.
109
+ */
110
+ export function xCallWeight(method: string, path: string): number {
111
+ const { bare, query } = splitQuery(path);
112
+ return rateBudgetWeight(VENDOR, `${String(method).toUpperCase()} ${bare}`, query);
113
+ }
114
+
115
+ function splitQuery(path: string): { bare: string; query: Record<string, string> } {
116
+ const at = path.indexOf('?');
117
+ const query: Record<string, string> = {};
118
+ if (at !== -1) for (const [k, v] of new URLSearchParams(path.slice(at + 1))) query[k] = v;
119
+ const raw = at === -1 ? path : path.slice(0, at);
120
+ const bare = raw.length > 1 && raw.endsWith('/') ? raw.replace(/\/+$/, '') : raw;
121
+ return { bare, query };
122
+ }
123
+
124
+ /** Where this vendor's ledger lives. Token-keyed and cwd-independent by default; pass `root` to
125
+ * opt into world-scoped accounting. */
126
+ export function xBudgetPath(opts: { root?: string; token?: string } | string = {}): string {
127
+ const o = typeof opts === 'string' ? { root: opts } : opts;
128
+ // VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through must not
129
+ // redirect this pack's ledger.
130
+ return rateBudgetPath({ ...o, vendor: VENDOR });
131
+ }
132
+
133
+ /** Construction options. The vendor is fixed; everything else may only TIGHTEN. */
134
+ export type XBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
135
+
136
+ /**
137
+ * This vendor's budget — the shared kernel guard bound to this declaration. A real subclass, not
138
+ * an alias, so `budget instanceof XBudget` means "a budget that accounts against X's ledger under
139
+ * X's ceiling".
140
+ */
141
+ export class XBudget extends RateBudget {
142
+ constructor(opts: XBudgetOptions = {}) {
143
+ super({ ...opts, vendor: VENDOR });
144
+ }
145
+ }
146
+
147
+ export { RateBudgetError as XBudgetError } from '@volter/world-core';
148
+ export type { RateBudgetErrorKind as XBudgetErrorKind } from '@volter/world-core';
149
+ export type XBudgetReservation = RateBudgetReservation;
150
+ export type XBudgetSnapshot = RateBudgetSnapshot;