yappr 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 (149) hide show
  1. package/.env.example +115 -0
  2. package/config/context/personality.md +7 -0
  3. package/config/context/security.md +10 -0
  4. package/config/hooks/example.ts +47 -0
  5. package/config/hooks/holder.ts +154 -0
  6. package/config/hooks/user-memory.ts +102 -0
  7. package/config/skills/compute/handler.ts +6 -0
  8. package/config/skills/compute/skill.md +7 -0
  9. package/config/skills/cron/handler.ts +89 -0
  10. package/config/skills/cron/skill.md +36 -0
  11. package/config/skills/generate-image/handler.ts +133 -0
  12. package/config/skills/generate-image/skill.md +20 -0
  13. package/config/skills/generate-meme-prompt/handler.ts +40 -0
  14. package/config/skills/generate-meme-prompt/skill.md +23 -0
  15. package/config/skills/stats/handler.ts +76 -0
  16. package/config/skills/stats/skill.md +18 -0
  17. package/config/skills/wallet/handler.ts +56 -0
  18. package/config/skills/wallet/skill.md +17 -0
  19. package/config/skills/x/handler.ts +135 -0
  20. package/config/skills/x/skill.md +163 -0
  21. package/dist/config/hooks/example.d.ts +2 -0
  22. package/dist/config/hooks/example.js +37 -0
  23. package/dist/config/hooks/holder.d.ts +2 -0
  24. package/dist/config/hooks/holder.js +147 -0
  25. package/dist/config/hooks/user-memory.d.ts +2 -0
  26. package/dist/config/hooks/user-memory.js +79 -0
  27. package/dist/config/skills/compute/handler.d.ts +2 -0
  28. package/dist/config/skills/compute/handler.js +5 -0
  29. package/dist/config/skills/cron/handler.d.ts +2 -0
  30. package/dist/config/skills/cron/handler.js +84 -0
  31. package/dist/config/skills/generate-image/handler.d.ts +2 -0
  32. package/dist/config/skills/generate-image/handler.js +122 -0
  33. package/dist/config/skills/generate-meme/handler.d.ts +2 -0
  34. package/dist/config/skills/generate-meme/handler.js +121 -0
  35. package/dist/config/skills/generate-meme-prompt/handler.d.ts +2 -0
  36. package/dist/config/skills/generate-meme-prompt/handler.js +38 -0
  37. package/dist/config/skills/stats/handler.d.ts +2 -0
  38. package/dist/config/skills/stats/handler.js +71 -0
  39. package/dist/config/skills/wallet/handler.d.ts +2 -0
  40. package/dist/config/skills/wallet/handler.js +54 -0
  41. package/dist/config/skills/x/handler.d.ts +2 -0
  42. package/dist/config/skills/x/handler.js +115 -0
  43. package/dist/src/agent-prompt.d.ts +1 -0
  44. package/dist/src/agent-prompt.js +45 -0
  45. package/dist/src/bankr.d.ts +41 -0
  46. package/dist/src/bankr.js +76 -0
  47. package/dist/src/cli/backup.d.ts +7 -0
  48. package/dist/src/cli/backup.js +78 -0
  49. package/dist/src/cli/charts.d.ts +32 -0
  50. package/dist/src/cli/charts.js +222 -0
  51. package/dist/src/cli/config-sync.d.ts +7 -0
  52. package/dist/src/cli/config-sync.js +71 -0
  53. package/dist/src/cli/deploy.d.ts +2 -0
  54. package/dist/src/cli/deploy.js +1059 -0
  55. package/dist/src/cli/env.d.ts +4 -0
  56. package/dist/src/cli/env.js +50 -0
  57. package/dist/src/cli/host-key.d.ts +4 -0
  58. package/dist/src/cli/host-key.js +50 -0
  59. package/dist/src/cli/index.d.ts +2 -0
  60. package/dist/src/cli/index.js +71 -0
  61. package/dist/src/cli/init.d.ts +1 -0
  62. package/dist/src/cli/init.js +51 -0
  63. package/dist/src/cli/ssh.d.ts +2 -0
  64. package/dist/src/cli/ssh.js +141 -0
  65. package/dist/src/cli/status.d.ts +7 -0
  66. package/dist/src/cli/status.js +1184 -0
  67. package/dist/src/cli/tui.d.ts +18 -0
  68. package/dist/src/cli/tui.js +115 -0
  69. package/dist/src/cli/ui.d.ts +30 -0
  70. package/dist/src/cli/ui.js +164 -0
  71. package/dist/src/cli/update.d.ts +1 -0
  72. package/dist/src/cli/update.js +263 -0
  73. package/dist/src/cli/x-login.d.ts +6 -0
  74. package/dist/src/cli/x-login.js +70 -0
  75. package/dist/src/compute.d.ts +11 -0
  76. package/dist/src/compute.js +109 -0
  77. package/dist/src/config-loader.d.ts +19 -0
  78. package/dist/src/config-loader.js +82 -0
  79. package/dist/src/config.d.ts +29 -0
  80. package/dist/src/config.js +68 -0
  81. package/dist/src/cron/capability.d.ts +6 -0
  82. package/dist/src/cron/capability.js +66 -0
  83. package/dist/src/cron/runner.d.ts +2 -0
  84. package/dist/src/cron/runner.js +113 -0
  85. package/dist/src/cron/schedule.d.ts +19 -0
  86. package/dist/src/cron/schedule.js +154 -0
  87. package/dist/src/cron/store.d.ts +46 -0
  88. package/dist/src/cron/store.js +220 -0
  89. package/dist/src/db.d.ts +4 -0
  90. package/dist/src/db.js +53 -0
  91. package/dist/src/hooks/loader.d.ts +1 -0
  92. package/dist/src/hooks/loader.js +17 -0
  93. package/dist/src/hooks/registry.d.ts +17 -0
  94. package/dist/src/hooks/registry.js +78 -0
  95. package/dist/src/hooks/types.d.ts +45 -0
  96. package/dist/src/hooks/types.js +1 -0
  97. package/dist/src/index.d.ts +25 -0
  98. package/dist/src/index.js +35 -0
  99. package/dist/src/llm/index.d.ts +23 -0
  100. package/dist/src/llm/index.js +213 -0
  101. package/dist/src/llm/prompts.d.ts +6 -0
  102. package/dist/src/llm/prompts.js +99 -0
  103. package/dist/src/log.d.ts +2 -0
  104. package/dist/src/log.js +30 -0
  105. package/dist/src/reply/agent.d.ts +20 -0
  106. package/dist/src/reply/agent.js +215 -0
  107. package/dist/src/reply/context-blocks.d.ts +12 -0
  108. package/dist/src/reply/context-blocks.js +22 -0
  109. package/dist/src/reply/gating.d.ts +3 -0
  110. package/dist/src/reply/gating.js +35 -0
  111. package/dist/src/reply/pipeline.d.ts +3 -0
  112. package/dist/src/reply/pipeline.js +144 -0
  113. package/dist/src/reply/poller.d.ts +5 -0
  114. package/dist/src/reply/poller.js +79 -0
  115. package/dist/src/skills/holder-access.d.ts +7 -0
  116. package/dist/src/skills/holder-access.js +53 -0
  117. package/dist/src/skills/loader.d.ts +2 -0
  118. package/dist/src/skills/loader.js +64 -0
  119. package/dist/src/skills/registry.d.ts +4 -0
  120. package/dist/src/skills/registry.js +10 -0
  121. package/dist/src/skills/types.d.ts +16 -0
  122. package/dist/src/skills/types.js +1 -0
  123. package/dist/src/state.d.ts +5 -0
  124. package/dist/src/state.js +26 -0
  125. package/dist/src/stats-cli.d.ts +1 -0
  126. package/dist/src/stats-cli.js +82 -0
  127. package/dist/src/stats.d.ts +41 -0
  128. package/dist/src/stats.js +236 -0
  129. package/dist/src/storage.d.ts +16 -0
  130. package/dist/src/storage.js +107 -0
  131. package/dist/src/treasury/abi.d.ts +99 -0
  132. package/dist/src/treasury/abi.js +71 -0
  133. package/dist/src/treasury/cycle.d.ts +16 -0
  134. package/dist/src/treasury/cycle.js +154 -0
  135. package/dist/src/treasury/index.d.ts +28 -0
  136. package/dist/src/treasury/index.js +222 -0
  137. package/dist/src/util.d.ts +3 -0
  138. package/dist/src/util.js +18 -0
  139. package/dist/src/wallet.d.ts +5 -0
  140. package/dist/src/wallet.js +241 -0
  141. package/dist/src/x/client.d.ts +74 -0
  142. package/dist/src/x/client.js +323 -0
  143. package/dist/src/x/types.d.ts +61 -0
  144. package/dist/src/x/types.js +1 -0
  145. package/dist/src/x402.d.ts +6 -0
  146. package/dist/src/x402.js +11 -0
  147. package/dist/src/yappr.d.ts +1 -0
  148. package/dist/src/yappr.js +85 -0
  149. package/package.json +52 -0
@@ -0,0 +1,241 @@
1
+ import { config } from "./config.js";
2
+ import { log } from "./log.js";
3
+ import { bankrApi, bankrX402Pay } from "./bankr.js";
4
+ import { createBankrSigner, createPayFetch } from "./x402.js";
5
+ import { resolveEvmAddress } from "./compute.js";
6
+ import { recordSpend } from "./stats.js";
7
+ import { sleep, envNumber } from "./util.js";
8
+ let _walletAddress = null;
9
+ let _payFetch = null;
10
+ export async function initBankr() {
11
+ // `/wallet/me` returns the EVM wallet under `wallets[]` (the Bankr EIP-7702 wallet
12
+ // has no top-level `address`), so resolve it the same robust way as everywhere else
13
+ // — and throw clearly if it's missing, rather than silently storing `undefined`.
14
+ _walletAddress = await resolveEvmAddress(config.bankrApiKey);
15
+ return _walletAddress;
16
+ }
17
+ export function walletAddress() {
18
+ if (!_walletAddress)
19
+ throw new Error("initBankr() not called yet");
20
+ return _walletAddress;
21
+ }
22
+ function requestUrl(input) {
23
+ if (typeof input === "string")
24
+ return input;
25
+ if (input instanceof URL)
26
+ return input.toString();
27
+ return input.url;
28
+ }
29
+ // Redact credentials carried as query params (auth_token/ct0) before logging a URL.
30
+ function redactUrl(url) {
31
+ try {
32
+ const u = new URL(url);
33
+ for (const k of ["auth_token", "ct0"]) {
34
+ if (u.searchParams.has(k))
35
+ u.searchParams.set(k, "[redacted]");
36
+ }
37
+ return u.toString();
38
+ }
39
+ catch {
40
+ return url;
41
+ }
42
+ }
43
+ // Atomic USDC amount paid for a given Response, captured from the X-PAYMENT header
44
+ // the x402 client attaches to its (paid) retry request. Keyed on the Response so
45
+ // concurrent paid calls can't clobber each other's amount. wrapFetchWithPayment
46
+ // returns the paid response object directly, so the lookup in payFetch hits.
47
+ const _paidAtomic = new WeakMap();
48
+ // Read the EIP-3009 `authorization.value` (atomic USDC) out of a base64 X-PAYMENT /
49
+ // PAYMENT-SIGNATURE header so we can report what each call actually cost.
50
+ function paymentAtomic(headers) {
51
+ const raw = headers.get("x-payment") ?? headers.get("payment-signature");
52
+ if (!raw)
53
+ return undefined;
54
+ try {
55
+ const decoded = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
56
+ const value = decoded?.payload?.authorization?.value;
57
+ return value != null ? BigInt(value) : undefined;
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ // Base fetch that records the paid amount per response, then delegates to fetch.
64
+ const tracedFetch = async (input, init) => {
65
+ const res = await fetch(input, init);
66
+ // wrapFetchWithPayment sends a Request object (headers on it, no init) on the
67
+ // paid retry; a first, unpaid attempt carries no payment header and is ignored.
68
+ const headers = input instanceof Request ? input.headers : new Headers(init?.headers);
69
+ const atomic = paymentAtomic(headers);
70
+ if (atomic != null)
71
+ _paidAtomic.set(res, atomic);
72
+ return res;
73
+ };
74
+ // Client-side x402 paid fetch: the @x402/fetch client signs the EIP-3009 payment
75
+ // authorization locally (via Bankr /wallet/sign) and sends the X-PAYMENT header
76
+ // itself — instead of delegating the whole flow to Bankr's /wallet/x402-pay
77
+ // gateway. Built lazily (after initBankr() resolves the wallet address) and reused.
78
+ function paidFetch() {
79
+ if (!_payFetch) {
80
+ const signer = createBankrSigner(config.bankrApiKey, walletAddress());
81
+ _payFetch = createPayFetch(signer, tracedFetch);
82
+ }
83
+ return _payFetch;
84
+ }
85
+ // Safety cap on what the /wallet/x402-pay fallback may authorize per call — used
86
+ // verbatim when the 402's payment requirements can't be parsed, and as a ceiling
87
+ // when they can (guards against an endpoint demanding an absurd amount).
88
+ const FALLBACK_MAX_USD = envNumber("X402_FALLBACK_MAX_USD", 5);
89
+ const FALLBACK_ATTEMPTS = 2;
90
+ // USD price asked by a 402 response: the base64 payment-required header (Bankr
91
+ // style) or the JSON body (Coinbase x402 style), whichever parses.
92
+ async function requiredUsd(res) {
93
+ let req;
94
+ const raw = res.headers.get("payment-required") ?? res.headers.get("x-payment-required");
95
+ if (raw) {
96
+ try {
97
+ req = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
98
+ }
99
+ catch { /* try body */ }
100
+ }
101
+ if (!req) {
102
+ try {
103
+ req = await res.clone().json();
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ const accept = req?.accepts?.[0];
110
+ const atomic = accept?.maxAmountRequired ?? accept?.amount;
111
+ const n = Number(atomic);
112
+ return atomic != null && Number.isFinite(n) ? n / 1e6 : undefined;
113
+ }
114
+ // Repackage a /wallet/x402-pay gateway result as a Response so callers see the
115
+ // same shape as the client-side path, including the paid amount for paidUsd().
116
+ function gatewayResponse(result) {
117
+ const body = typeof result.response === "string" ? result.response : JSON.stringify(result.response ?? null);
118
+ const res = new Response(body, {
119
+ status: result.status || (result.success ? 200 : 402),
120
+ headers: typeof result.response === "string" ? undefined : { "Content-Type": "application/json" },
121
+ });
122
+ if (result.paymentMade?.amountUsd) {
123
+ _paidAtomic.set(res, BigInt(Math.round(result.paymentMade.amountUsd * 1e6)));
124
+ }
125
+ return res;
126
+ }
127
+ // Which spend category an x402 call bills to, by host: the compute API
128
+ // (compute.x402layer.cc) and the X data endpoint (x402.twit.sh) keep their own lines;
129
+ // everything else a skill/hook pays for (image gen, other x402 APIs) rolls up under "x402".
130
+ function spendCategory(url) {
131
+ if (url.includes("compute.x402layer.cc"))
132
+ return "compute";
133
+ if (url.includes("twit.sh"))
134
+ return "x-api";
135
+ return "x402";
136
+ }
137
+ export async function payFetch(input, init = {}) {
138
+ const url = requestUrl(input);
139
+ const method = (init.method ?? "GET").toUpperCase();
140
+ let res;
141
+ let clientErr;
142
+ try {
143
+ res = await paidFetch()(input, init);
144
+ }
145
+ catch (err) {
146
+ clientErr = err;
147
+ }
148
+ // Fallback: a 402 left after the client-side payment retry (or a thrown payment
149
+ // error) is usually Bankr having EIP-7702-delegated the wallet to a smart account
150
+ // after a server-side op (transfer/swap/fee claim) — the EOA then has code, USDC
151
+ // validates EIP-3009 signatures via ERC-1271 on the delegate, and the plain
152
+ // /wallet/sign signature is rejected. Bankr's /wallet/x402-pay gateway clears the
153
+ // delegation and pays in one call, so route the same request through it.
154
+ if (!res || res.status === 402) {
155
+ const maxUsd = Math.min((res && (await requiredUsd(res))) ?? FALLBACK_MAX_USD, FALLBACK_MAX_USD);
156
+ log.warn({ url: redactUrl(url), method, status: res?.status, maxUsd, err: clientErr instanceof Error ? clientErr.message : clientErr }, "x402 client-side payment failed — falling back to Bankr /wallet/x402-pay");
157
+ for (let attempt = 1; attempt <= FALLBACK_ATTEMPTS; attempt++) {
158
+ try {
159
+ const result = await bankrX402Pay(config.bankrApiKey, url, method, typeof init.body === "string" ? init.body : undefined, maxUsd);
160
+ if (result.success) {
161
+ res = gatewayResponse(result);
162
+ break;
163
+ }
164
+ log.warn({ url: redactUrl(url), attempt, status: result.status, err: result.error }, "x402-pay fallback failed");
165
+ }
166
+ catch (err) {
167
+ log.warn({ url: redactUrl(url), attempt, err: err instanceof Error ? err.message : String(err) }, "x402-pay fallback errored");
168
+ }
169
+ }
170
+ // Both paths exhausted: surface the original client-side failure — the 402
171
+ // response if there was one, otherwise the thrown error.
172
+ if (!res)
173
+ throw clientErr;
174
+ }
175
+ // /tweets/mentions is polled constantly — skip its success log to keep logs
176
+ // clean. Failures are always logged. The per-call USD cost is captured per
177
+ // response (see paidUsd) and logged by the caller (x/client, treasury) on the
178
+ // line that's actually shown, so the amount appears next to the call itself.
179
+ const isMentions = url.includes("/tweets/mentions");
180
+ const safeUrl = redactUrl(url);
181
+ // warn, not error: this layer rethrows nothing itself but every caller throws on
182
+ // !res.ok and the failure is logged as ONE error where it's finally handled —
183
+ // logging error here too would count a single failure 2-3× in the stats ledger.
184
+ if (!res.ok)
185
+ log.warn({ url: safeUrl, method, status: res.status }, "x402 payFetch failed");
186
+ else if (!isMentions)
187
+ log.info({ url: safeUrl, method, status: res.status }, "x402 payFetch ok");
188
+ // Every x402 payment funnels through here, so it's the one place to record spend
189
+ // into the ledger. Categorise by host: the compute API, the X data endpoint, or any
190
+ // other x402 endpoint a skill/hook calls (image gen, etc.) → the generic "x402".
191
+ // (Inference isn't x402 — it's recorded separately from the credit balance.)
192
+ const paid = paidUsd(res);
193
+ if (paid != null && paid > 0)
194
+ recordSpend(spendCategory(url), paid);
195
+ return res;
196
+ }
197
+ // USD cost (USDC, 6 decimals) of the paid call that produced `res`, or undefined if
198
+ // the response wasn't a paid one. Callers log this on their own success line so the
199
+ // amount shows up where the call is reported (e.g. "x-api GET ... ok {usd}").
200
+ export function paidUsd(res) {
201
+ const atomic = _paidAtomic.get(res);
202
+ return atomic != null ? Number(atomic) / 1e6 : undefined;
203
+ }
204
+ // Fixed pause before each submit so rapid back-to-back treasury txs (dev cut → burn →
205
+ // swap → extend) don't trip the Bankr signer's in-flight limit; and attempts/backoff for
206
+ // retrying transient signer/provider errors (provider_inflight_limit, 5xx, timeouts, …).
207
+ const SUBMIT_PAUSE_MS = envNumber("TX_SUBMIT_PAUSE_MS", 1500);
208
+ const SUBMIT_MAX_ATTEMPTS = envNumber("TX_SUBMIT_MAX_ATTEMPTS", 5);
209
+ export async function submitTx(to, data) {
210
+ if (config.treasuryDryRun) {
211
+ log.info({ to, data: data.slice(0, 66) + "..." }, "bankr [dry run] submitTx");
212
+ return "0xdry000000000000000000000000000000000000000000000000000000000000";
213
+ }
214
+ // Space submissions out, then retry any failure with exponential backoff (2s, 4s, 8s,
215
+ // 16s) — gives a busy signer time to clear before the next attempt.
216
+ await sleep(SUBMIT_PAUSE_MS);
217
+ let lastErr;
218
+ for (let attempt = 1; attempt <= SUBMIT_MAX_ATTEMPTS; attempt++) {
219
+ try {
220
+ // Bankr's /wallet/submit expects the tx nested under `transaction` (not flat) and
221
+ // returns `transactionHash`.
222
+ const res = await bankrApi(config.bankrApiKey, "/wallet/submit", {
223
+ method: "POST",
224
+ body: JSON.stringify({
225
+ transaction: { to, data, chainId: 8453 },
226
+ waitForConfirmation: true,
227
+ }),
228
+ });
229
+ return res.transactionHash ?? res.txHash ?? "";
230
+ }
231
+ catch (err) {
232
+ lastErr = err;
233
+ if (attempt < SUBMIT_MAX_ATTEMPTS) {
234
+ const delayMs = 2000 * 2 ** (attempt - 1); // 2s → 4s → 8s → 16s
235
+ log.warn({ to, attempt, maxAttempts: SUBMIT_MAX_ATTEMPTS, delayMs, err: err instanceof Error ? err.message : String(err) }, "submitTx failed — retrying after backoff");
236
+ await sleep(delayMs);
237
+ }
238
+ }
239
+ }
240
+ throw lastErr;
241
+ }
@@ -0,0 +1,74 @@
1
+ import type { SearchResponse, Tweet } from "./types.js";
2
+ export declare function extractTweetId(raw: string): string;
3
+ export declare function tweetImageUrls(tweet: Tweet): string[];
4
+ export declare function getTweetById(id: string): Promise<Tweet>;
5
+ export declare function getTweets(ids: string[]): Promise<Tweet[]>;
6
+ export declare function getUserTweets(username: string, next_token?: string): Promise<SearchResponse>;
7
+ export type TweetSearchParams = {
8
+ words?: string;
9
+ phrase?: string;
10
+ anyWords?: string;
11
+ noneWords?: string;
12
+ hashtags?: string;
13
+ from?: string;
14
+ to?: string;
15
+ mentioning?: string;
16
+ minReplies?: number;
17
+ minLikes?: number;
18
+ minReposts?: number;
19
+ since?: string;
20
+ until?: string;
21
+ next_token?: string;
22
+ };
23
+ export declare function searchTweets(params: TweetSearchParams): Promise<SearchResponse>;
24
+ export declare function searchMentions(handle: string): Promise<SearchResponse>;
25
+ export declare function getTweetReplies(id: string, next_token?: string): Promise<SearchResponse>;
26
+ export declare function getRetweetedBy(id: string, next_token?: string): Promise<SearchResponse>;
27
+ export declare function getQuoteTweets(id: string, next_token?: string): Promise<SearchResponse>;
28
+ export declare function postTweet(text: string, opts?: {
29
+ replyTo?: string;
30
+ quoteTweetId?: string;
31
+ mediaIds?: string[];
32
+ }): Promise<void>;
33
+ export declare function postReply(tweetId: string, text: string, mediaIds?: string[]): Promise<void>;
34
+ export declare function deleteTweet(id: string): Promise<void>;
35
+ export declare function likeTweet(id: string): Promise<void>;
36
+ export declare function unlikeTweet(id: string): Promise<void>;
37
+ export declare function retweetTweet(id: string): Promise<void>;
38
+ export declare function unretweetTweet(id: string): Promise<void>;
39
+ export declare function bookmarkTweet(id: string): Promise<void>;
40
+ export declare function unbookmarkTweet(id: string): Promise<void>;
41
+ export declare function uploadMedia(file: Blob | ArrayBuffer | Uint8Array, opts?: {
42
+ filename?: string;
43
+ contentType?: string;
44
+ }): Promise<string>;
45
+ export declare function uploadMediaFromUrl(url: string): Promise<string>;
46
+ export declare function getUserByUsername(username: string): Promise<unknown>;
47
+ export declare function getUserById(id: string): Promise<unknown>;
48
+ export declare function searchUsers(query: string, next_token?: string): Promise<unknown>;
49
+ export declare function getUsers(ids: string[]): Promise<unknown>;
50
+ export declare function getFollowers(id: string, next_token?: string): Promise<unknown>;
51
+ export declare function getFollowing(id: string, next_token?: string): Promise<unknown>;
52
+ export declare function followUser(opts: {
53
+ id?: string;
54
+ username?: string;
55
+ }): Promise<void>;
56
+ export declare function unfollowUser(opts: {
57
+ id?: string;
58
+ username?: string;
59
+ }): Promise<void>;
60
+ export declare function setProfile(opts: {
61
+ name?: string;
62
+ bio?: string;
63
+ location?: string;
64
+ url?: string;
65
+ }): Promise<void>;
66
+ export declare function getArticle(id: string): Promise<unknown>;
67
+ export declare function getList(id: string): Promise<unknown>;
68
+ export declare function getListMembers(id: string, next_token?: string): Promise<unknown>;
69
+ export declare function getListFollowers(id: string, next_token?: string): Promise<unknown>;
70
+ export declare function getListTweets(id: string, next_token?: string): Promise<SearchResponse>;
71
+ export declare function getCommunity(id: string): Promise<unknown>;
72
+ export declare function getCommunityMembers(id: string, next_token?: string): Promise<unknown>;
73
+ export declare function getCommunityPosts(id: string, next_token?: string): Promise<SearchResponse>;
74
+ export declare function getUserInsights(username: string): Promise<unknown>;
@@ -0,0 +1,323 @@
1
+ import { payFetch, paidUsd } from "../wallet.js";
2
+ import { config } from "../config.js";
3
+ import { log } from "../log.js";
4
+ import { sleep } from "../util.js";
5
+ // Full X/Twitter API SDK over the x402-paid data endpoint. Not every export is
6
+ // wired to a skill — these are the building blocks skills (e.g. config/skills/x)
7
+ // compose into actions.
8
+ // ─── helpers ────────────────────────────────────────────────────────────────
9
+ function base(path) {
10
+ return `${config.xApiBaseUrl}${path}`;
11
+ }
12
+ function auth() {
13
+ return { auth_token: config.twitterAuthToken, ct0: config.twitterCt0 };
14
+ }
15
+ // twit.sh endpoints that act on behalf of the authenticated user. They require
16
+ // auth_token + ct0, which we inject automatically (from env) on every call — these
17
+ // params are never exposed to the LLM/skill. Read endpoints don't need auth.
18
+ const AUTHENTICATED_PATHS = new Set([
19
+ "/tweets",
20
+ "/tweets/long",
21
+ "/tweets/like",
22
+ "/tweets/bookmark",
23
+ "/tweets/retweet",
24
+ "/users/following",
25
+ "/users/setProfile",
26
+ // (/tweets/mediaUpload is also authenticated, but uploadMedia injects auth itself —
27
+ // it's multipart/form-data and bypasses the JSON post()/withAuth() path.)
28
+ ]);
29
+ function withAuth(path, params) {
30
+ return AUTHENTICATED_PATHS.has(path) ? { ...params, ...auth() } : params;
31
+ }
32
+ function buildUrl(path, params) {
33
+ const url = new URL(base(path));
34
+ for (const [k, v] of Object.entries(params)) {
35
+ if (v !== undefined)
36
+ url.searchParams.set(k, String(v));
37
+ }
38
+ return url.toString();
39
+ }
40
+ async function get(path, params = {}) {
41
+ const t = Date.now();
42
+ log.info({ path, params: sanitizeParams(params) }, `x-api GET ${path}`);
43
+ const res = await payFetch(buildUrl(path, params));
44
+ if (!res.ok) {
45
+ const body = await res.text();
46
+ // warn before throwing: the catch site logs the (counted) error — see log.ts.
47
+ log.warn({ path, status: res.status, ms: Date.now() - t }, `x-api GET ${path} failed`);
48
+ throw new Error(`GET ${path} failed: ${res.status} ${body}`);
49
+ }
50
+ log.info({ path, status: res.status, ms: Date.now() - t, usd: paidUsd(res) }, `x-api GET ${path} ok`);
51
+ return res.json();
52
+ }
53
+ async function post(path, body) {
54
+ const payload = withAuth(path, body);
55
+ // twit.sh proxy endpoints read their params (including auth_token/ct0) from the
56
+ // QUERY STRING, not the body — confirmed: a correctly-sent body-only POST still
57
+ // returns "ct0 is null". So put the payload on the URL (and keep it in the body
58
+ // too for any endpoint that reads it, e.g. /tweets/long's `text`).
59
+ const url = buildUrl(path, payload);
60
+ for (let attempt = 1; attempt <= 5; attempt++) {
61
+ const t = Date.now();
62
+ log.info({ path, attempt }, `x-api POST ${path}`);
63
+ const res = await payFetch(url, {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json" },
66
+ body: JSON.stringify(payload),
67
+ });
68
+ // twit.sh returns 2xx (often 201) even when the action failed — the real
69
+ // signal is an `errors` array embedded in the body (e.g. a created tweet has
70
+ // `data.id`, a failure has `data.errors` like the 186 length/auth error). So
71
+ // on a 2xx we inspect the body and treat an embedded error as a failure.
72
+ if (res.ok) {
73
+ const json = await res.json().catch(() => undefined);
74
+ const apiErrors = json?.errors ?? json?.data?.errors;
75
+ if (Array.isArray(apiErrors) && apiErrors.length > 0) {
76
+ log.warn({ path, status: res.status, ms: Date.now() - t, errors: apiErrors }, `x-api POST ${path} returned errors`);
77
+ throw new Error(`POST ${path} failed: ${JSON.stringify(apiErrors)}`);
78
+ }
79
+ // Log the raw twit.sh response body (e.g. the created tweet on POST /tweets).
80
+ log.info({ path, status: res.status, ms: Date.now() - t, usd: paidUsd(res), response: json }, `x-api POST ${path} ok`);
81
+ return json;
82
+ }
83
+ const text = await res.text();
84
+ if (res.status === 500 && attempt < 5) {
85
+ log.warn({ path, attempt, status: res.status }, `x-api POST ${path} 500, retrying`);
86
+ await sleep(attempt * 1000);
87
+ continue;
88
+ }
89
+ log.warn({ path, status: res.status, ms: Date.now() - t, body: text }, `x-api POST ${path} failed`);
90
+ throw new Error(`POST ${path} failed: ${res.status} ${text}`);
91
+ }
92
+ throw new Error(`POST ${path} exhausted retries`);
93
+ }
94
+ async function del(path, params) {
95
+ const all = withAuth(path, params);
96
+ const t = Date.now();
97
+ log.info({ path }, `x-api DELETE ${path}`);
98
+ const res = await payFetch(buildUrl(path, all), { method: "DELETE" });
99
+ if (!res.ok) {
100
+ const body = await res.text();
101
+ log.warn({ path, status: res.status, ms: Date.now() - t }, `x-api DELETE ${path} failed`);
102
+ throw new Error(`DELETE ${path} failed: ${res.status} ${body}`);
103
+ }
104
+ log.info({ path, status: res.status, ms: Date.now() - t, usd: paidUsd(res) }, `x-api DELETE ${path} ok`);
105
+ return res.json();
106
+ }
107
+ function sanitizeParams(params) {
108
+ const safe = {};
109
+ for (const [k, v] of Object.entries(params)) {
110
+ if (k === "auth_token" || k === "ct0")
111
+ safe[k] = "[redacted]";
112
+ else
113
+ safe[k] = v;
114
+ }
115
+ return safe;
116
+ }
117
+ // ─── tweets ─────────────────────────────────────────────────────────────────
118
+ export function extractTweetId(raw) {
119
+ return raw.match(/\/status\/(\d+)/)?.[1] ?? raw.trim();
120
+ }
121
+ // Direct CDN URLs of the still images attached to a tweet (photos only — video /
122
+ // animated_gif are skipped). Reads entities.media first, falling back to the
123
+ // parallel media_metadata list, and dedupes. Used by the reply loop to attach the
124
+ // image to a vision model when a mention carries one.
125
+ export function tweetImageUrls(tweet) {
126
+ const urls = new Set();
127
+ for (const m of tweet.entities?.media ?? []) {
128
+ if ((m.type ?? "photo") === "photo" && m.media_url_https)
129
+ urls.add(m.media_url_https);
130
+ }
131
+ for (const m of tweet.media_metadata ?? []) {
132
+ if (m.media_url)
133
+ urls.add(m.media_url);
134
+ }
135
+ return [...urls];
136
+ }
137
+ export async function getTweetById(id) {
138
+ return get("/tweets/by/id", { id });
139
+ }
140
+ export async function getTweets(ids) {
141
+ if (ids.length === 0)
142
+ return [];
143
+ log.info({ ids }, "GET x-api /tweets");
144
+ const json = await get("/tweets", { ids: ids.join(",") });
145
+ return json.data ?? [];
146
+ }
147
+ export async function getUserTweets(username, next_token) {
148
+ return get("/tweets/user", { username, next_token });
149
+ }
150
+ export async function searchTweets(params) {
151
+ return get("/tweets/search", params);
152
+ }
153
+ // Poll the authenticated account's mentions. Uses the purpose-built
154
+ // /tweets/mentions endpoint, which returns the same shape as search but only
155
+ // needs auth (auth_token + ct0). `type` is an optional filter we leave unset.
156
+ // Poll for mentions using the method chosen in config (POLL_METHOD):
157
+ // - "search": /tweets/search filtered to tweets mentioning the agent handle.
158
+ // - "mentions": the dedicated /tweets/mentions endpoint (auth_token + ct0 only).
159
+ // Both return the same SearchResponse shape.
160
+ export async function searchMentions(handle) {
161
+ // No banner log here — the underlying get()/searchTweets already logs the request
162
+ // and its "ok" line (with cost), so a separate line would just triple the output.
163
+ if (config.pollMethod === "mentions") {
164
+ return get("/tweets/mentions", auth());
165
+ }
166
+ return searchTweets({ mentioning: handle });
167
+ }
168
+ export async function getTweetReplies(id, next_token) {
169
+ return get("/tweets/replies", { id, next_token });
170
+ }
171
+ export async function getRetweetedBy(id, next_token) {
172
+ return get("/tweets/retweeted_by", { id, next_token });
173
+ }
174
+ export async function getQuoteTweets(id, next_token) {
175
+ return get("/tweets/quote_tweets", { id, next_token });
176
+ }
177
+ export async function postTweet(text, opts = {}) {
178
+ // /tweets/long supports the full character limit; plain /tweets rejects longer text.
179
+ // Both accept `medias` — a comma-separated list of media IDs from uploadMedia() — to
180
+ // attach images to the post.
181
+ await post("/tweets/long", {
182
+ text,
183
+ ...(opts.replyTo ? { in_reply_to_tweet_id: opts.replyTo } : {}),
184
+ ...(opts.quoteTweetId ? { quote_tweet_id: opts.quoteTweetId } : {}),
185
+ ...(opts.mediaIds?.length ? { medias: opts.mediaIds.join(",") } : {}),
186
+ });
187
+ }
188
+ export async function postReply(tweetId, text, mediaIds) {
189
+ log.info({ tweetId, media: mediaIds?.length ?? 0 }, "POST x-api reply");
190
+ await postTweet(text, { replyTo: tweetId, mediaIds });
191
+ }
192
+ export async function deleteTweet(id) {
193
+ await del("/tweets", { id });
194
+ }
195
+ export async function likeTweet(id) {
196
+ await post("/tweets/like", { id });
197
+ }
198
+ export async function unlikeTweet(id) {
199
+ await del("/tweets/like", { id });
200
+ }
201
+ export async function retweetTweet(id) {
202
+ await post("/tweets/retweet", { id });
203
+ }
204
+ export async function unretweetTweet(id) {
205
+ await del("/tweets/retweet", { id });
206
+ }
207
+ export async function bookmarkTweet(id) {
208
+ await post("/tweets/bookmark", { id });
209
+ }
210
+ export async function unbookmarkTweet(id) {
211
+ await del("/tweets/bookmark", { id });
212
+ }
213
+ // ─── media ─────────────────────────────────────────────────────────────────
214
+ // Upload an image to X and return its media_id — attach it to a post via
215
+ // postTweet({ mediaIds: [id] }). This endpoint is multipart/form-data with auth on the
216
+ // query string (per the twit.sh spec), so it bypasses the JSON post() helper and builds
217
+ // the request itself. Accepts raw bytes (Blob / Uint8Array / ArrayBuffer).
218
+ export async function uploadMedia(file, opts = {}) {
219
+ const path = "/tweets/mediaUpload";
220
+ const url = buildUrl(path, auth()); // auth_token + ct0 on the query string
221
+ // Cast at the Blob boundary: TS 5.7's generic Uint8Array<ArrayBufferLike> doesn't
222
+ // structurally match lib.dom's BlobPart (ArrayBufferView<ArrayBuffer>), though the
223
+ // bytes are valid — see microsoft/TypeScript#59417.
224
+ const blob = file instanceof Blob ? file : new Blob([file], { type: opts.contentType ?? "application/octet-stream" });
225
+ const form = new FormData();
226
+ form.append("file", blob, opts.filename ?? "image.png");
227
+ const t = Date.now();
228
+ log.info({ path }, `x-api POST ${path}`);
229
+ // No Content-Type header: fetch sets multipart/form-data with the correct boundary.
230
+ const res = await payFetch(url, { method: "POST", body: form });
231
+ if (!res.ok) {
232
+ const body = await res.text();
233
+ log.warn({ path, status: res.status, ms: Date.now() - t }, `x-api POST ${path} failed`);
234
+ throw new Error(`POST ${path} failed: ${res.status} ${body}`);
235
+ }
236
+ const json = (await res.json().catch(() => undefined));
237
+ // The success body is undocumented in the spec; pull the id from the usual
238
+ // Twitter/twit.sh field names (media_id_string is the canonical Twitter one).
239
+ const id = json?.media_id_string ?? json?.media_id ?? json?.data?.media_id_string ?? json?.data?.media_id ?? json?.id ?? json?.data?.id;
240
+ log.info({ path, status: res.status, ms: Date.now() - t, usd: paidUsd(res), mediaId: id }, `x-api POST ${path} ok`);
241
+ if (id == null)
242
+ throw new Error(`mediaUpload: no media id in response ${JSON.stringify(json)}`);
243
+ return String(id);
244
+ }
245
+ // Convenience: fetch an image by URL (plain fetch — public CDN, not x402) and upload it,
246
+ // returning the media_id. Pairs with image-generation skills that produce a hosted URL.
247
+ export async function uploadMediaFromUrl(url) {
248
+ const res = await fetch(url, { signal: AbortSignal.timeout(20_000) });
249
+ if (!res.ok)
250
+ throw new Error(`fetch media ${url} failed: ${res.status}`);
251
+ const contentType = res.headers.get("content-type") ?? "image/png";
252
+ const bytes = new Uint8Array(await res.arrayBuffer());
253
+ const filename = new URL(url).pathname.split("/").pop() || "image.png";
254
+ return uploadMedia(bytes, { filename, contentType });
255
+ }
256
+ // ─── users ──────────────────────────────────────────────────────────────────
257
+ export async function getUserByUsername(username) {
258
+ return get("/users/by/username", { username });
259
+ }
260
+ export async function getUserById(id) {
261
+ return get("/users/by/id", { id });
262
+ }
263
+ export async function searchUsers(query, next_token) {
264
+ return get("/users/search", { query, next_token });
265
+ }
266
+ // Batch-fetch several users in one call. Cheaper than N `getUserById` calls when
267
+ // you already have the numeric ids (e.g. resolving a list of authors/followers).
268
+ export async function getUsers(ids) {
269
+ return get("/users", { ids: ids.join(",") });
270
+ }
271
+ export async function getFollowers(id, next_token) {
272
+ return get("/users/followers", { id, next_token });
273
+ }
274
+ export async function getFollowing(id, next_token) {
275
+ return get("/users/following", { id, next_token });
276
+ }
277
+ export async function followUser(opts) {
278
+ await post("/users/following", opts);
279
+ }
280
+ export async function unfollowUser(opts) {
281
+ await del("/users/following", opts);
282
+ }
283
+ // Update the authenticated user's profile. Only the fields you pass are sent; an
284
+ // omitted field is left unchanged (pass an empty string to clear one).
285
+ export async function setProfile(opts) {
286
+ await post("/users/setProfile", {
287
+ ...(opts.name !== undefined ? { name: opts.name } : {}),
288
+ ...(opts.bio !== undefined ? { bio: opts.bio } : {}),
289
+ ...(opts.location !== undefined ? { location: opts.location } : {}),
290
+ ...(opts.url !== undefined ? { url: opts.url } : {}),
291
+ });
292
+ }
293
+ // ─── articles ───────────────────────────────────────────────────────────────
294
+ export async function getArticle(id) {
295
+ return get("/articles/by/id", { id });
296
+ }
297
+ // ─── lists ──────────────────────────────────────────────────────────────────
298
+ export async function getList(id) {
299
+ return get("/lists/by/id", { id });
300
+ }
301
+ export async function getListMembers(id, next_token) {
302
+ return get("/lists/members", { id, next_token });
303
+ }
304
+ export async function getListFollowers(id, next_token) {
305
+ return get("/lists/followers", { id, next_token });
306
+ }
307
+ export async function getListTweets(id, next_token) {
308
+ return get("/lists/tweets", { id, next_token });
309
+ }
310
+ // ─── communities ─────────────────────────────────────────────────────────────
311
+ export async function getCommunity(id) {
312
+ return get("/communities/by/id", { id });
313
+ }
314
+ export async function getCommunityMembers(id, next_token) {
315
+ return get("/communities/members", { id, next_token });
316
+ }
317
+ export async function getCommunityPosts(id, next_token) {
318
+ return get("/communities/posts", { id, next_token });
319
+ }
320
+ // ─── workflows ───────────────────────────────────────────────────────────────
321
+ export async function getUserInsights(username) {
322
+ return get("/workflows/userInsights", { username });
323
+ }